2009-02-18 7 views
4

J'ai une méthode qui contient un appel asynchrone comme celui-ci:Comment puis-je tester une méthode contenant un appel asynchrone?

public void MyMethod() { 
    ... 
    (new Action<string>(worker.DoWork)).BeginInvoke(myString, null, null); 
    ... 
} 

J'utilise l'unité et la création d'objets fantaisie ne sont pas un problème, mais comment puis-je vérifier que DoWork est appelé sans se soucier des conditions de course?

Un previous question offre une solution, mais il me semble que wait-handles est un hack (la condition de course est toujours là, bien qu'il devrait être pratiquement impossible à élever).


EDIT: D'accord, je pensais que je pourrais poser cette question d'une manière très générale, mais il semble que je dois élaborer davantage sur le problème:

Je veux créer un test pour mentionné ci-dessus MyMethod, donc je fais quelque chose comme ceci:

[TestMethod] 
public void TestMyMethod() { 
    ...setup... 
    MockWorker worker = new MockWorker(); 
    MyObject myObj = new MyObject(worker); 
    ...assert preconditions... 
    myObj.MyMethod(); 
    ...assert postconditions... 
} 

L'approche naïve serait de créer un MockWorker() qui définit simplement un drapeau quand DoWork a été appelé, et vérifier que le drapeau dans les postconditions. Cela conduirait bien sûr à une condition de concurrence, où les postconditions sont vérifiées avant que le drapeau ne soit placé dans le MockWorker.

L'approche la plus correcte (que je vais probablement à l'aide) avec une attente poignée:

... et utiliser l'affirmation suivante:

Assert.IsTrue(worker.AutoResetEvent.WaitOne(1000, false)); 

C'est en utilisant une approche de type sémaphore, ce qui est bien ... mais dans la théorie ce qui pourrait arriver:

  1. BeginInvoke est appelée sur mon délégué DoWork
  2. Pour une raison quelconque, le thread principal ou le thread DoWork ne reçoit pas de temps d'exécution de 1000ms.
  3. Le thread principal reçoit un temps d'exécution, et en raison du délai d'attente, l'assertion échoue, même si le thread DoWork doit encore être exécuté.

Ai-je mal compris comment fonctionne AutoResetEvent? Suis-je juste trop paranoïaque, ou y a-t-il une solution intelligente à ce problème?

+0

Pouvez-vous expliquer les conditions de concurrence dans cette situation? C'est à peu près la solution que j'utilise et ça ne m'a jamais manqué. –

+0

Non, je suis sûr que ça n'échoue jamais, parce que la condition de la course est principalement théorique. La condition de concurrence est simple: si, pour une raison quelconque, le thread de travail ne reçoit jamais de temps d'exécution avant le timeout, le test échoue à tort. Je sais! C'est spéculatif, mais je connais la loi de Murphy ;-) – toxvaerd

+0

J'ai vu des tests qui utilisaient des drapeaux comme ceux-ci échouer sur des serveurs de build lourdement chargés (nécessitant des reconstructions qui ajoutent à la charge). Aller sur la route de signalisation et utiliser un timeout me semble mieux. En outre, l'utilisation de drapeaux signifie que vous devez dormir pendant toute la période, ce qui peut rendre votre suite de tests beaucoup plus longue que nécessaire. –

Répondre

3

Attendre le handle serait comme je le ferais. AFAIK (et je ne sais pas avec certitude) les méthodes asynchrones utilisent des poignées d'attente pour déclencher cette méthode de toute façon. Je ne suis pas sûr de savoir pourquoi vous pensez que la situation de concurrence se produirait, sauf si vous donnez un temps anormalement court sur l'appel WaitOne. Je mettrais 4-5 secondes sur le waitone, de cette façon vous sauriez avec certitude si c'était cassé et ce n'était pas juste une course.

Aussi, ne pas oublier comment attendre des poignées travailler, aussi longtemps que la poignée d'attente est créé, vous pouvez avoir l'ordre de l'exécution d'un

  • Discussion 1 - créer attendre poignée
  • Discussion 1 - ensemble attente poignée
  • discussion 2 - WaitOne sur la poignée d'attente
  • discussion 2 - coups à travers la poignée d'attente et l'exécution continue

Même si l'exécution normale est

  • Discussion 1 - créer attendre poignée
  • Discussion 2 - WaitOne sur la poignée d'attente
  • Discussion 1 - ensemble attente poignée
  • discussion 2 - coups à travers la poignée d'attente et continue l'exécution

Soit fonctionne correctement, la poignée d'attente peut être définie avant Thread2 commence à attendre et tout est géré pour vous.

+0

Pouvez-vous élaborer à ce sujet? Je sais que la condition de course est hautement théorique, mais il semble que c'est quelque chose qui irait mal le jour où nous embarquons ;-) – toxvaerd

+0

J'ai mis à jour le post, le vérifier et me contacter –

3

Lors du test d'un délégué direct, utilisez simplement l'appel EndInvoke pour vous assurer que le délégué est appelé et renvoie la valeur appropriée. Par exemple.

var del = new Action<string>(worker.DoWork); 
var async = del.BeginInvoke(myString,null,null); 
... 
var result = del.EndInvoke(async); 

EDIT

OP ont fait remarquer qu'ils tentent de MyMethod de tests unitaires par rapport à la méthode worker.DoWork.

Dans ce cas, vous devrez compter sur un effet secondaire visible en appelant la méthode DoWork. Sur la base de votre exemple, je ne peux pas vraiment en dire trop car aucun des mécanismes internes de DoWork n'est exposé.

EDIT2

[OP] Pour une raison quelconque, ni le fil principal ou le DoWork-thread est temps d'exécution pour 1000ms.

Cela n'arrivera pas. Lorsque vous appelez WaitOne sur un AutoResetEvent, le thread se met en veille. Il ne recevra aucune heure de processeur jusqu'à ce que l'événement soit défini ou que la période d'expiration expire. Il est définitivement possible qu'un autre thread reçoive une tranche de temps significative et provoque un faux échec. Mais je pense que c'est assez improbable. J'ai plusieurs tests qui fonctionnent de la même manière et je ne reçois pas beaucoup de faux échecs comme celui-ci. Je choisis habituellement un délai d'attente de ~ 2 minutes.

+0

Je ne teste pas la méthode du délégué lui-même - Je suis en train de tester la méthode invoquant le délégué. – toxvaerd

+0

Depuis que j'utilise des objets simulés, je peux choisir les effets secondaires de DoWork() moi-même ... Comme je l'ai dit - je fais des tests unitaires MyMethod et non DoWork, donc le fonctionnement interne de DoWork ne devrait pas être pertinent. – toxvaerd

+0

@Toxvaerd, vous devrez alors clarifier votre question. La seule chose que nous pouvons dire de votre message est que vous avez une méthode appelée MyMethod qui appelle de façon asynchrone un délégué et vous voulez savoir si elle est appelée. Il est impossible d'aider sans plus de détails – JaredPar

2

Voici ce que j'ai fait dans ces situations: Créer un service qui prend le délégué et l'exécute (exposer cela via une interface simple, injecter où vous appelez des choses de manière asynchrone).

Dans le service réel, il sera exécuté avec BeginInvoke et donc de manière asynchrone. Créez ensuite une version du service à tester qui appelle le délégué de manière synchrone.

Voici un exemple:

public interface IActionRunner 
{ 
    void Run(Action action, AsyncCallback callback, object obj); 
    void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj); 
    void Run<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2, AsyncCallback callback, object obj); 
    void Run<T1, T2, T3>(Action<T1, T2, T3> action, T1 arg1, T2 arg2, T3 arg3, AsyncCallback callback, object obj); 
    void Run<T1, T2, T3, T4>(Action<T1, T2, T3, T4> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, AsyncCallback callback, object obj); 
} 

Une implémentation Asycnhronous de ce service ressemble à ceci:

public void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj) 
{ 
    action.BeginInvoke(arg, callback, obj); 
} 

Une implémentation synchrone de ce service ressemble à ceci:

public void Run<T>(Action<T> action, T arg, AsyncCallback callback, object obj) 
{ 
    action(arg); 
} 

Lorsque vous testez des unités, si vous utilisez quelque chose comme RhinoMocks et AutoMoc conteneur roi, vous pouvez échanger dans votre ActionRunner synchrone:

_mocks = new MockRepository(); 

_container = new AutoMockingContainer(_mocks); 
_container.AddService(typeof(IActionRunner), new SyncActionRunner()); 
_container.Initialize(); 

Le ActionRunner n'a pas besoin d'être testé; c'est juste un mince placage sur l'appel de la méthode.

+1

Cela ne va-t-il pas simplement bouger le problème? Que se passe-t-il lorsque je veux tester le service? – toxvaerd

+0

Personnellement, ne vous inquiétez pas de tester les fonctionnalités de .Net. Je m'attends à ce que l'appel asynch fonctionne sans problème, si vous comprenez ce que je dis. Tester BeginInvoke() ne me semble pas être un test très utile. –

+0

Le service est un emballage mince; Je vais éditer mon post pour que vous puissiez voir. –

Questions connexes