2010-02-25 3 views
7

Ich habe ein Problem beim Testen einer Klasse, die Ereignisse auslöst, wenn ein Thread gestartet und beendet wird. Eine abgespeckte Version der betreffenden Quelle ist wie folgt:Unit Testen eines Ereignisses, das von einem Thread ausgelöst wird

public class ThreadRunner 
{ 
    private bool keepRunning; 

    public event EventHandler Started; 
    public event EventHandler Finished; 

    public void StartThreadTest() 
    { 
     this.keepRunning = true; 
     var thread = new Thread(new ThreadStart(this.LongRunningMethod)); 
     thread.Start(); 
    } 

    public void FinishThreadTest() 
    { 
     this.keepRunning = false; 
    } 

    protected void OnStarted() 
    { 
     if (this.Started != null) 
      this.Started(this, new EventArgs()); 
    } 

    protected void OnFinished() 
    { 
     if (this.Finished != null) 
      this.Finished(this, new EventArgs()); 
    } 

    private void LongRunningMethod() 
    { 
     this.OnStarted(); 

     while (this.keepRunning) 
      Thread.Sleep(100); 

     this.OnFinished(); 
    } 
} 

ich, dass die wie folgt abgeschlossen Finished Ereignis ausgelöst wird, nachdem die LongRunningMethod hat dann einen Test habe zu überprüfen: So ist die Idee hier

[TestClass] 
public class ThreadRunnerTests 
{ 
    [TestMethod] 
    public void CheckFinishedEventFiresTest() 
    { 
     var threadTest = new ThreadRunner(); 

     bool finished = false; 

     object locker = new object(); 

     threadTest.Finished += delegate(object sender, EventArgs e) 
     { 
      lock (locker) 
      { 
       finished = true; 
       Monitor.Pulse(locker); 
      } 
     }; 

     threadTest.StartThreadTest(); 
     threadTest.FinishThreadTest(); 

     lock (locker) 
     { 
      Monitor.Wait(locker, 1000); 
      Assert.IsTrue(finished); 
     } 
    } 
} 

dass der Test für maximal eine Sekunde blockiert wird - oder bis das Ereignis Finish ausgelöst wird - bevor geprüft wird, ob das Flag finished gesetzt ist.

Offensichtlich habe ich etwas falsch gemacht, da manchmal der Test bestanden wird, manchmal wird es nicht. Das Debugging scheint sehr schwierig zu sein, und die Breakpoints, die ich erwarten würde (z. B. die Methode OnFinished), scheinen nicht immer zu sein.

Ich nehme an, dies ist nur mein Missverständnis der Art, wie Threading funktioniert, so dass hoffentlich jemand mich erleuchten kann.

+0

Danke für alle Antworten. Ich habe auch einen anderen Fehler eingeführt, indem ich die Thread-Worker-Methode (d. H. LongRunningMethod) beim Start ein eigenes Steuerflag gesetzt habe. Heute war sicherlich ein Crash-Kurs, wie man Race Conditions in deinen Code einführt, doh! – Dougc

Antwort

15

Schloß hier einfach nicht geeignet ist, sollten Sie ein Ereignis signalisieren. Zum Beispiel:

public void CheckFinishedEventFiresTest() { 
     var threadTest = new ThreadRunner(); 
     var finished = new ManualResetEvent(false); 
     threadTest.Finished += delegate(object sender, EventArgs e) { 
      finished.Set(); 
     }; 
     threadTest.StartThreadTest(); 
     threadTest.FinishThreadTest(); 
     Assert.IsTrue(finished.WaitOne(1000)); 
    } 
2

Ihr Test scheint falsch zu sein. Nehmen Sie an, dass nach threadTest.FinishThreadTest(); Sperre durch den Code in CheckFinishedEventFiresTest() erhalten wird. Dann wird der Test fehlschlagen. Du hast hier eine klare Race Condition.

Beachten Sie, dass die Rückkehr von FinishThreadTest() nicht Garantie, dass der Thread abgeschlossen ist. Es setzt nur die Flagge für den Thread, die in jedem Moment berücksichtigt werden kann (nichts garantiert grundsätzlich, dass der Thread sofort vom Scheduler ausgeführt wird).

In Ihrem Fall ist der Thread höchstwahrscheinlich beschäftigt Sleep() ing. Nach dem Aufruf threadTest.FinishThreadTest(); wird die Sperre höchstwahrscheinlich von dem Thread übernommen, in dem CheckFinishedEventFiresTest() ausgeführt wird. Der Monitor wartet 1 Sekunde und gibt dann auf. Danach wird die Sperre aufgehoben, sodass der Delegierte nur in diesem Moment sperren kann.

+0

Ah, macht jetzt sicherlich Sinn. Danke, Vlad. – Dougc

4

Vlad ist absolut richtig, aber ich werde einen weiteren Schuss, um das Problem zu klären:

// This runs on the other thread 
threadTest.Finished += delegate(object sender, EventArgs e) { 
    // I can't get this lock if the test thread gets here first! 
    lock (locker) { 
     finished = true; 
     Monitor.Pulse(locker); 
    } 
}; 

Sie es mit einer Warte Griff irgendeiner Art zu tun. Ich würde verwenden eine ManualResetEvent:

ManualResetEvent waitHandle = new ManualResetEvent(false); 
threadTest.Finished += delegate(object sender, EventArgs e) { 
    finished = true; 
    waitHandle.Set(); 
}; 

threadTest.StartThreadTest(); 
threadTest.FinishThreadTest(); 

// Specify a timeout so your test isn't hostage forever 
if (waitHandle.WaitOne(timeout, true)) { 
    Assert.IsTrue(finished); 
} 
+0

Danke Jeff. Es scheint immer noch ein seltsames Problem zu sein, bei dem der Thread manchmal nicht beendet wird, nachdem das keepRunning-Flag durch die FinishThreadTest-Methode umgeschaltet wurde? Selbst wenn ich das Timeout auf einen sehr hohen Wert erhöhe. – Dougc

+0

'LongRunningMethod' wird nicht beendet? Vielleicht gibt es ein lustiges Geschäft mit dem zweiten WaitOne-Parameter (exit context). Ich würde versuchen, nobugz 'Probe, die das gleiche Ziel zu erreichen, aber ist sowieso besser (mit dem überflüssigen bool). –

+0

Egal, ich war ein Idiot (siehe meinen Kommentar zu der ursprünglichen Frage). Vielen Dank! – Dougc

3

Ich schrieb vor kurzem eine Reihe von Blog-Posts auf Unit-Tests Ereignissequenzen für Objekte, die sowohl synchrone als auch asynchrone Ereignisse veröffentlichen. Die Posts beschreiben einen Einheitentestansatz und ein Framework und liefern den vollständigen Quellcode mit Tests.

AsyncEventPublisher publisher = new AsyncEventPublisher(); 

Action test =() => 
{ 
    publisher.RaiseA(); 
    publisher.RaiseB(); 
    publisher.RaiseC(); 
}; 

var expectedSequence = new[] { "EventA", "EventB", "EventC" }; 

EventMonitor.Assert(test, publisher, expectedSequence, TimeoutMS); 

Der EventMonitor macht die ganze schwere Arbeit und wird den Test (Aktion) und behauptet, dass die Ereignisse ausgelöst werden in der erwarteten Sequenz (expectedSequence) läuft:

die Rahmen-Tests verwenden, kann wie so geschrieben werden. Es verarbeitet asynchrone Ereignisse und gibt bei fehlgeschlagenen Tests nette Diagnosemeldungen aus.

Es gibt viele Details in den Beiträgen die Themen und Ansätze beschreiben, und Source-Code zu:

http://gojisoft.com/blog/2010/04/22/event-sequence-unit-testing-part-1/