C #: Comment tester une classe de travailleurs threadés de base

J’essaie de tester des trucs multi-thread, mais je ne sais pas trop comment commencer. Je suis sûr que je trouverai plus de choses plus facilement si je pouvais tout simplement commencer, alors je me demandais si quelqu’un pourrait m’aider à écrire un scénario de test NUnit pour ce cours simple:

class Worker { public event EventHandler Done = (s, e) => { }; public void StartWork() { var thread = new Thread(Work) { Name = "Worker Thread" }; thread.Start(); } private void Work() { // Do some heavy lifting Thread.Sleep(500); Done(this, EventArgs.Empty); } } 

Ce que je voudrais tester est simplement: l’événement Done est-il déclenché à la fin? Je n’aurais aucun problème si c’était synchrone, mais je ne savais pas par où commencer, même si ce n’était pas le cas. Un simple test s’il n’était pas multi-thread (et que la méthode de Work n’était pas privée) pourrait être:

 [TestFixture] class WorkerTests { [Test] public void DoWork_WhenDone_EventIsRaised() { var worker = new Worker(); var eventWasRaised = false; worker.Done += (s, e) => eventWasRaised = true; worker.Work(); Assert.That(eventWasRaised); } } 

Des pointeurs?

Vous devez utiliser un ManualResetEvent – voir Unité de test des événements asynchrones à plusieurs threads pour plus de détails.

Quelque chose comme:

 [Test] public void DoWork_WhenDone_EventIsRaised() { var worker = new Worker(); var eventWasRaised = false; var mre = new ManualResetEvent(false); worker.Done += (s, e) => { eventWasRaised= true; mre.Set(); }; worker.Work(); mre.WaitOne(1000); Assert.That(eventWasRaised); } 

Le principal problème que vous rencontrez lors du test des applications à threads est en réalité de stimuler le thread avec des données de test, car vous devrez bloquer sur le thread principal pour attendre que l’autre thread se ferme.

Nous avons utilisé cette méthode pour la tester de manière synchrone, comme vous le suggérez. Cela vous permet de tester le comportement logique, mais il ne détectera bien sûr pas les blocages et les conditions de concurrence (de toute manière, les tests ne permettent pas d’affirmer ces choses facilement).

Il peut y avoir deux options ici: 1) Ajouter une méthode Wait au travailleur afin que vous puissiez attendre l’achèvement de la tâche 2) Au lieu d’un simple object d’événement d’utilisation booléenne (AutoResetEvent)

En règle générale, chaque attente doit attendre le délai spécifié. Dans les exemples ci-dessous, l’attente est infinie.

Première option:

 class Worker { //... Thread thread; public void StartWork() { thread = new Thread(Work) { Name = "Worker Thread" }; thread.Start(); } void WaitCompletion() { if ( thread != null ) thread.Join(); } //... } [TestFixture] class WorkerTests { [Test] public void DoWork_WhenDone_EventIsRaised() { var worker = new Worker(); var eventWasRaised = false; worker.Done += (s, e) => eventWasRaised = true; worker.Work(); worker.WaitCompletion(); Assert.That(eventWasRaised); } } 

Deuxième option: (l’attente peut être effectuée avec timeout)

 [TestFixture] class WorkerTests { [Test] public void DoWork_WhenDone_EventIsRaised() { var worker = new Worker(); AutoResetEvent eventWasRaised = new AutoResetEvent(false); worker.Done += (s, e) => eventWasRaised.Set(); worker.Work(); Assert.That(eventWasRaised.WaitOne()); } } 

Vous pouvez utiliser un modèle commun qui expose la création de thread à la classe externe.

Dans la classe, extrayez la création du thread en méthode virtuelle:

 class Worker { public event EventHandler Done = (s, e) => { }; public void StartWork() { var thread = CreateThread(); thread.Start(); } // Seam for extension and testability virtual protected Thread CreateThread() { return new Thread(Work) { Name = "Worker Thread" }; } private void Work() { // Do some heavy lifting Thread.Sleep(500); Done(this, EventArgs.Empty); } } 

Définissez la sous-classe qui expose le thread:

 class WorkerForTest : Worker { internal Thread thread; protected override Thread CreateThread() { thread = base.CreateThread(); return thread; } } 

Synchronisez le test avec le thread:

 [TestFixture] class WorkerTests { [Test] public void DoWork_WhenDone_EventIsRaised() { var worker = new WorkerForTest(); var eventWasRaised = false; worker.Done += (s, e) => eventWasRaised = true; worker.StartWork(); // Use the seam for synchronizing the thread in the test worker.thread.Join(); Assert.That(eventWasRaised); } } 

Ce cas de conception pour la testabilité présente des avantages par rapport à la synchronisation du fil de test en le mettant en veille avant Assert:

  • Il n’y aura pas de faux échecs négatifs comme cela pourrait être le cas lorsque vous mettez le thread de test en veille pendant une période qui est généralement suffisante pour que le thread de travail se termine.
  • Il ne fonctionnera pas plus lentement que nécessaire, car le temps de veille prend un tampon pour en être sûr. Ceci est important lorsque de nombreux tests de la suite en dépendent.