2010-01-21 17 views
19

En el siguiente ejemplo de código, tengo una clase de calculadora Async. Esto se inyecta con un ICalc, que será una calculadora sincrónica. Utilizo la inyección de la dependencia y me burlo del ICalc porque esto se parece a mi verdadero escenario, aunque supongo que la burla no es realmente relevante para la pregunta. El AsyncCalc tiene una función que llamará a otra función de forma asíncrona, tomando una devolución de llamada como parámetro. Y cuando finaliza la llamada a la función asincrónica, la devolución de llamada se activará con el resultado.Prueba de la unidad función asíncrona

Ahora quiero probar mi función asíncrona, comprobando que la devolución de llamada se desencadena con el parámetro esperado. Este código parece funcionar. Sin embargo, creo que podría explotar en cualquier momento, y mi preocupación es que la condición de carrera de la devolución de llamada finalice antes de que la función finalice y la prueba finalice, ya que esto se ejecutará en un hilo separado.

Mi pregunta ahora es si estoy en la unidad de pista correcta probando la función asíncrona, o si alguien puede ayudarme a seguir el camino correcto ...? Lo que se sentiría mejor es si pudiera garantizar que la devolución de llamada se desencadene de inmediato, ¿y preferiblemente en el mismo hilo, supongo? ¿Se puede/debe hacerse?

public interface ICalc 
{ 
    int AddNumbers(int a, int b); 
} 

public class AsyncCalc 
{ 
    private readonly ICalc _calc; 
    public delegate void ResultProcessor(int result); 
    public delegate int AddNumbersAsyncCaller(int a, int b); 

    public AsyncCalc(ICalc calc) 
    { 
     _calc = calc; 
    } 

    public void AddNumbers(int a, int b, ResultProcessor resultProcessor) 
    { 
     var caller = new AddNumbersAsyncCaller(_calc.AddNumbers); 
     caller.BeginInvoke(a, b, new AsyncCallback(AddNumbersCallbackMethod), resultProcessor); 
    } 

    public void AddNumbersCallbackMethod(IAsyncResult ar) 
    { 
     var result = (AsyncResult)ar; 
     var caller = (AddNumbersAsyncCaller)result.AsyncDelegate; 
     var resultFromAdd = caller.EndInvoke(ar); 

     var resultProcessor = ar.AsyncState as ResultProcessor; 
     if (resultProcessor == null) return; 

     resultProcessor(resultFromAdd); 
    }    
} 

[Test] 
public void TestingAsyncCalc() 
{ 
    var mocks = new MockRepository(); 
    var fakeCalc = mocks.DynamicMock<ICalc>(); 

    using (mocks.Record()) 
    { 
     fakeCalc.AddNumbers(1, 2); 
     LastCall.Return(3); 
    } 

    var asyncCalc = new AsyncCalc(fakeCalc); 
    asyncCalc.AddNumbers(1, 2, TestResultProcessor); 
} 

public void TestResultProcessor(int result) 
{ 
    Assert.AreEqual(3, result); 
} 
+1

yo creo que la prueba simplemente tendría que esperar en un bucle hasta que la devolución de llamada se ejecuta. –

Respuesta

20

Puede usar un ManualResetEvent para sincronizar sus hilos.

En el siguiente ejemplo, el hilo de prueba se bloqueará en la llamada al completion.WaitOne(). La devolución de llamada para el cálculo asíncrono almacena el resultado y luego señala el evento llamando al completion.Set().

[Test] 
public void TestingAsyncCalc() 
{ 
    var mocks = new MockRepository(); 
    var fakeCalc = mocks.DynamicMock<ICalc>(); 

    using (mocks.Record()) 
    { 
     fakeCalc.AddNumbers(1, 2); 
     LastCall.Return(3); 
    } 

    var asyncCalc = new AsyncCalc(fakeCalc); 

    var completion = new ManualResetEvent(false); 
    int result = 0; 
    asyncCalc.AddNumbers(1, 2, r => { result = r; completion.Set(); }); 
    completion.WaitOne(); 

    Assert.AreEqual(3, calcResult); 
} 

// ** USING AN ANONYMOUS METHOD INSTEAD 
// public void TestResultProcessor(int result) 
// { 
//  Assert.AreEqual(3, result); 
// } 
+0

¡Freakin increíble! Funciona perfectamente. Justo lo que necesitaba. ¡Gracias! – stiank81

+3

Solo agregaría que para ciertos escenarios (servicio externo) es posible que desee agregar un tiempo de espera: Assert.IsTrue (completion.WaitOne (TimeSpan.FromSeconds (10)), "Cálculo completado"); – Rashack

0

También podría utilizar una clase de "corredor de pruebas" para ejecutar las afirmaciones en un bucle. El ciclo ejecutaría los asertos en un try/catch. El controlador de excepción simplemente intentará ejecutar las afirmaciones nuevamente hasta que expire el tiempo de espera. Recientemente escribí una publicación en un blog sobre esta técnica. El ejemplo está en groovy, pero es aplicable a cualquier idioma. En lugar de pasar un Cierre, pasaría una Acción en C#.

http://www.greenmoonsoftware.com/2013/08/asynchronous-functional-testing/