2010-02-25 10 views
7

Estoy teniendo problemas para probar una clase que dispara eventos cuando un hilo se inicia y termina. Una versión reducida de la fuente infractor es el siguiente:Unidad Comprobación de un evento de un hilo

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

entonces tengo una prueba para comprobar que los Finished desencadena el evento después de la LongRunningMethod ha terminado de la siguiente manera:

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

Así que la idea aquí ya que la prueba se bloqueará durante un máximo de un segundo, o hasta que se active el evento Finish, antes de verificar si se ha establecido el indicador finished.

Es evidente que he hecho algo mal, ya que a veces la prueba pasará, a veces no lo hará. La depuración parece muy difícil, así como los puntos de ruptura que esperaría que me tocara (el método OnFinished, por ejemplo) no siempre parece ser así.

Supongo que esta es solo mi incomprensión de la forma en que funciona el enhebrado, así que espero que alguien pueda iluminarme.

+0

Gracias por todas las respuestas. También introduje otro error al hacer que el método thread worker (es decir, LongRunningMethod) establezca su propio indicador de control cuando se inicia. Hoy ciertamente ha sido un curso intensivo sobre cómo introducir condiciones de carrera en tu código, ¡do! – Dougc

Respuesta

15

Un bloqueo es simplemente no es apropiado aquí, tendrá que indicar a un evento. Por ejemplo:

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

Parece que la prueba es incorrecta. Suponga que después de threadTest.FinishThreadTest(); el código se obtiene con el código en CheckFinishedEventFiresTest(). Entonces la prueba va a fallar Aquí tienes una clara condición de carrera.

Tenga en cuenta que el retorno de FinishThreadTest() no garantiza que el hilo está terminado. Simplemente establece el indicador para el hilo, que se puede tener en cuenta en cualquier momento (básicamente, nada garantiza que el programa ejecute el hilo de inmediato).

En su caso, el hilo probablemente estará ocupado Sleep() ing. Después de llamar al threadTest.FinishThreadTest();, la cerradura será adquirida probablemente por el hilo donde se ejecuta CheckFinishedEventFiresTest(). El monitor esperará 1 segundo y luego se dará por vencido. Después de que se desbloqueará, el delegado podrá bloquear solo en ese momento.

+0

Ah, ciertamente tiene sentido ahora. Gracias Vlad. – Dougc

4

Vlad tiene toda la razón, pero me quedo con otra oportunidad de aclarar el 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); 
    } 
}; 

Puede hacerlo con un mango de espera de algún tipo. Que haría uso de 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

Gracias Jeff. Todavía parece haber un problema extraño en el que a veces el hilo no sale después de que el indicador de mantener la ejecución ha sido cambiado por el método FinishThreadTest. Incluso si aumenta el tiempo de espera a un valor muy alto. – Dougc

+0

'LongRunningMethod' no sale? Quizás haya algún negocio divertido con el segundo parámetro de WaitOne (contexto de salida). Probaría la muestra de nobugz, que pretende lograr lo mismo, pero de todos modos es mejor (prescindir del bool superfluo). –

+0

No importa, estaba siendo un idiota (ver mi comentario sobre la pregunta original). ¡Gracias! – Dougc

3

Hace poco escribió una serie de entradas de blog en las secuencias de eventos de pruebas unitarias para los objetos que publican ambos eventos sincrónicos y asincrónicos. Las publicaciones describen un enfoque y marco de prueba de unidades, y proporciona el código fuente completo con pruebas.

El uso de las pruebas de marco se puede escribir así:

AsyncEventPublisher publisher = new AsyncEventPublisher(); 

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

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

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

El EventMonitor hace todo el trabajo pesado y se ejecutará la prueba (acción) y afirmar que los eventos son criados en la secuencia esperada (expectedSequence). Maneja eventos asíncronos e imprime mensajes de diagnóstico agradables en fallas de prueba.

Hay una gran cantidad de detalle en los mensajes que describen los temas y enfoques, y el código fuente también:

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

Cuestiones relacionadas