2010-06-08 14 views
8

Estoy escribiendo pruebas de unidad para la capa "pegamento" de mi aplicación, y tengo dificultades para crear pruebas deterministas para métodos asíncronos que permiten al usuario cancelar la operación prematuramente.cancelación de orquestación en prueba de unidad de larga duración

Específicamente, en algunos métodos asíncronos tenemos un código que reacciona a la cancelación de la llamada y asegura que el objeto está en el estado correcto antes de completarlo. Me gustaría asegurarme de que estas rutas de código estén cubiertas por pruebas.

Algunos seudo código C# que ejemplifica un método asíncrono típico en este escenario es el siguiente:

public void FooAsync(CancellationToken token, Action<FooCompletedEventArgs> callback) 
{ 
    if (token.IsCancellationRequested) DoSomeCleanup0(); 

    // Call the four helper methods, checking for cancellations in between each 
    Exception encounteredException; 
    try 
    { 
     MyDependency.DoExpensiveStuff1(); 
     if (token.IsCancellationRequested) DoSomeCleanup1(); 

     MyDependency.DoExpensiveStuff2(); 
     if (token.IsCancellationRequested) DoSomeCleanup2(); 

     MyDependency.DoExpensiveStuff3(); 
     if (token.IsCancellationRequested) DoSomeCleanup3(); 

     MyDependency.DoExpensiveStuff4(); 
     if (token.IsCancellationRequested) DoSomeCleanup4(); 

    } 
    catch (Exception e) 
    { 
     encounteredException = e; 
    } 

    if (!token.IsCancellationRequested) 
    { 
     var args = new FooCompletedEventArgs(a bunch of params); 
     callback(args); 
    } 
} 

La solución que he encontrado hasta el momento implica burlarse de los MyDependency operaciones subyacentes que se envuelven por la capa de pegamento y obligando a cada uno a dormir durante un período de tiempo arbitrario. Luego invoco el método async y digo a mi prueba unitaria que duerma durante varios milisegundos antes de cancelar la solicitud asincrónica.

Algo como esto (usando burla de Rhino como ejemplo):

[TestMethod] 
public void FooAsyncTest_CancelAfter2() 
{ 
    // arrange 
    var myDependency = MockRepository.GenerateStub<IMyDependency>(); 

    // Set these stubs up to take a little bit of time each so we can orcestrate the cancels 
    myDependency.Stub(x => x.DoExpensiveStuff1()).WhenCalled(x => Thread.Sleep(100)); 
    myDependency.Stub(x => x.DoExpensiveStuff2()).WhenCalled(x => Thread.Sleep(100)); 
    myDependency.Stub(x => x.DoExpensiveStuff3()).WhenCalled(x => Thread.Sleep(100)); 
    myDependency.Stub(x => x.DoExpensiveStuff4()).WhenCalled(x => Thread.Sleep(100)); 

    // act 
    var target = new FooClass(myDependency); 

    CancellationTokenSource cts = new CancellationTokenSource(); 
    bool wasCancelled = false; 

    target.FooAsync(
     cts.Token, 
     args => 
     { 
     wasCancelled = args.IsCancelled; 
     // Some other code to manipulate FooCompletedEventArgs 
     }); 

    // sleep long enough for two operations to complete, then cancel 
    Thread.Sleep(250); 
    cts.Cancel(); 

    // Some code to ensure the async call completes goes here 

    //assert 
    Assert.IsTrue(wasCancelled); 
    // Other assertions to validate state of target go here 
} 

Aparte del hecho de que el uso de Thread.Sleep en una prueba de unidad me hace mareado, el mayor problema es que a veces las pruebas de este tipo fracasan en nuestro servidor de compilación si sucede que está bajo una carga significativa. La llamada asincrónica llega demasiado lejos y la cancelación llega demasiado tarde.

¿Alguien puede proporcionar una manera más confiable de probar la unidad lógica de cancelación para operaciones de larga ejecución como esta? Cualquier idea sería apreciada.

Respuesta

5

Intentaría utilizar simulaciones para "simular" el comportamiento asincrónico de forma síncrona. En lugar de utilizar

myDependency.Stub(x => x.DoExpensiveStuff1()).WhenCalled(x => Thread.Sleep(100)); 

y luego establecer el indicador de cancelación dentro de cualquier número de milisegundos, me acaba de establecer como parte de la devolución de llamada:

myDependency.Stub(x => x.DoExpensiveStuff1()); 
myDependency.Stub(x => x.DoExpensiveStuff2()); 
myDependency.Stub(x => x.DoExpensiveStuff3()).WhenCalled(x => cts.Cancel()); 
myDependency.Stub(x => x.DoExpensiveStuff4()); 

desde la perspectiva de su código de esto parecerá como si el la cancelación ocurrió durante la llamada.

+0

Esto se ve extremadamente prometedor. Lo probaré esta tarde y le informaré. –

+0

Funcionó muy bien. ¡Gracias por tu ayuda! –

1

Cada una de las operaciones de larga ejecución debe desencadenar un evento cuando comienzan a ejecutarse.

Enganche este evento en la prueba unitaria. Esto proporciona resultados deterministas con el potencial de que los eventos podrían ser útiles en el futuro.