2010-01-21 5 views
19

Dans l'exemple de code suivant, j'ai une classe Async Calculator. Ceci est injecté avec un ICalc, qui sera une calculatrice syncroneuse. J'utilise l'injection de dépendances et je me moque de l'ICalc parce que cela ressemble à mon vrai scénario, bien que je suppose que le moqueur n'est pas vraiment pertinent pour la question. L'AsyncCalc a une fonction qui appellera une autre fonction de manière asynchrone - en prenant un callback comme paramètre. Et lorsque l'appel de fonction async se termine, le rappel sera déclenché avec le résultat.Fonction de test unitaire asynchrone

Maintenant je veux tester ma fonction asynchrone - vérifier que le rappel est déclenché avec le paramètre attendu. Ce code semble fonctionner. Cependant, j'ai l'impression que ça pourrait exploser à tout moment - et je m'inquiète de la condition de course du rappel pour terminer avant que la fonction ne se termine et que le test soit terminé - car cela se déroulera dans un fil séparé.

Ma question est maintenant de savoir si je suis sur la bonne unité de test de la fonction asynchrone, ou si quelqu'un peut m'aider à prendre la bonne voie ..? Qu'est-ce qui se sentirait mieux, c'est si je pouvais m'assurer que le rappel est déclenché tout de suite - et de préférence sur le même fil, je suppose? Peut/Devrait-il être fait?

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

Je pense que votre test aurait simplement d'attendre dans une boucle jusqu'à ce que le rappel exécute. –

Répondre

20

Vous pouvez utiliser un ManualResetEvent pour synchroniser vos threads. Dans l'exemple suivant, le thread de test bloquera l'appel sur completion.WaitOne(). Le rappel pour le calcul asynchrone stocke le résultat et puis signale l'événement en appelant 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 génial! Fonctionne parfaitement. Juste ce dont j'avais besoin. THX! – stiank81

+3

Je voudrais seulement ajouter que pour certains scénarios (service externe), vous pouvez ajouter un délai: Assert.IsTrue (completion.WaitOne (TimeSpan.FromSeconds (10)), "Calcul terminé"); – Rashack

0

Vous pouvez également utiliser une classe "test runner" pour exécuter les asserts dans une boucle. La boucle exécutera les asserts dans un try/catch. Le gestionnaire d'exceptions essaierait simplement de réexécuter les assertions jusqu'à ce qu'un délai expire. J'ai récemment écrit un billet sur cette technique. L'exemple est dans groovy, mais est applicable à n'importe quelle langue. Au lieu de passer une fermeture, vous passeriez une action dans C#.

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