2010-02-25 15 views
7

Sto riscontrando un problema con un'unità che testa una classe che genera eventi all'avvio e al termine di un thread. Una versione ridotta della sorgente incriminata è la seguente:Unità di test di un evento da un thread

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(); 
    } 
} 

Ho poi ho un test per verificare che i Finished generato l'evento dopo il LongRunningMethod ha terminato come segue:

[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); 
     } 
    } 
} 

Quindi l'idea qui poiché il test si bloccherà per un massimo di un secondo o fino a quando l'evento Finish non viene attivato, prima di verificare se è impostato il flag finished.

Chiaramente ho fatto qualcosa di sbagliato a volte il test passerà, a volte no. Il debug sembra molto difficile così come i breakpoint che mi aspetto di essere colpiti (il metodo OnFinished, ad esempio) non sempre sembrano essere.

Sto assumendo che questo sia solo il mio fraintendimento del modo in cui funziona il threading, quindi spero che qualcuno possa illuminarmi.

+0

Grazie per tutte le risposte. Ho anche introdotto un altro problema avendo il metodo lavoratore filo (cioè LongRunningMethod) impostare il proprio flag di controllo all'avvio. Oggi è stato certamente un corso accelerato su come introdurre le condizioni di gara nel tuo codice, doh! – Dougc

risposta

15

Un blocco non è solo appropriato qui, ti consigliamo di segnalare un evento. Per esempio:

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

Il test sembra essere sbagliato. Si supponga che dopo il blocco threadTest.FinishThreadTest(); venga ottenuto dal codice in CheckFinishedEventFiresTest(). Quindi il test fallirà. Hai una chiara condizione di gara qui.

noti che di ritorno da FinishThreadTest() non si garanzia che il filo è terminato. Imposta solo il flag per il thread, che può essere preso in considerazione in qualsiasi momento (nulla garantisce sostanzialmente che il thread venga eseguito immediatamente dallo scheduler).

Nel tuo caso, il thread sarà probabilmente occupato Sleep() ing. Dopo aver chiamato threadTest.FinishThreadTest(); il blocco verrà probabilmente acquisito dal thread in cui viene eseguito CheckFinishedEventFiresTest(). Il monitor attenderà 1 secondo e poi si arrenderà. Dopo tale blocco verrà rilasciato, quindi il delegato sarà in grado di bloccare solo in quel momento.

+0

Ah, certamente ha senso ora. Grazie Vlad. – Dougc

4

Vlad è assolutamente giusto, ma mi prendo un altro colpo a chiarire il problema:

// 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); 
    } 
}; 

È possibile farlo con un manico di attesa di qualche tipo. Userei un 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

Grazie Jeff. Ci sembra ancora essere un problema strano in cui il filo a volte non esce dopo la bandiera keepRunning è stato attivato con il metodo FinishThreadTest? Anche se salgo il timeout a un valore molto alto. – Dougc

+0

'LongRunningMethod' non esce? Forse ci sono affari divertenti con il secondo parametro WaitOne (contesto di uscita). Proverò il campione di nobugz, che mira a realizzare la stessa cosa, ma è comunque migliore (eliminando il superfluo bool). –

+0

Non importa, mi era stato un idiota (vedi il mio commento alla domanda iniziale). Grazie! – Dougc

3

Recentemente ho scritto una serie di post di blog su sequenze di eventi di unit test per gli oggetti che pubblicano entrambi gli eventi sincroni e asincroni. I post descrivono un approccio e un framework di test unitario e forniscono il codice sorgente completo con test.

Utilizzando i test framework può essere scritto in questo modo:

AsyncEventPublisher publisher = new AsyncEventPublisher(); 

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

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

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

L'EventMonitor fa tutto il lavoro pesante, e si svolgerà il test (azione) e affermare che gli eventi vengono generati nella sequenza prevista (expectedSequence). Gestisce eventi asincroni e stampa buoni messaggi diagnostici in caso di fallimento del test.

C'è un sacco di dettagli nei messaggi che descrivono i problemi e gli approcci, e il codice sorgente troppo:

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

Problemi correlati