2010-04-02 8 views
12

Quiero probar que establecer una determinada propiedad (o, más en general, ejecutar algún código) plantea un determinado evento en mi objeto. En ese sentido, mi problema es similar al Unit testing that an event is raised in C#, pero necesito muchas de estas pruebas y odio las repeticiones. Así que estoy buscando una solución más general, utilizando la reflexión.Prueba de unidad de que un evento se produce en C#, utilizando la reflexión

Idealmente, me gustaría hacer algo como esto:

[TestMethod] 
public void TestWidth() { 
    MyClass myObject = new MyClass(); 
    AssertRaisesEvent(() => { myObject.Width = 42; }, myObject, "WidthChanged"); 
} 

Para la aplicación de la AssertRaisesEvent, he llegado hasta aquí:

private void AssertRaisesEvent(Action action, object obj, string eventName) 
{ 
    EventInfo eventInfo = obj.GetType().GetEvent(eventName); 
    int raisedCount = 0; 
    Action incrementer =() => { ++raisedCount; }; 
    Delegate handler = /* what goes here? */; 

    eventInfo.AddEventHandler(obj, handler); 
    action.Invoke(); 
    eventInfo.RemoveEventHandler(obj, handler); 

    Assert.AreEqual(1, raisedCount); 
} 

Como se puede ver, mi problema se basa en crear un Delegate del tipo apropiado para este evento. El delegado no debe hacer nada excepto invocar incrementer.

Debido a todo el jarabe sintáctico en C#, mi noción de cómo los delegados y los eventos realmente funcionan es un poco brumosa. Esta es también la primera vez que incursiono en la reflexión. ¿Cuál es la parte faltante?

Respuesta

7

Hace poco escribí 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.

Describo la implementación de un "monitor de eventos" que permite escribir las pruebas de la unidad de secuencia de eventos para que se escriban de forma más limpia, es decir, deshacerse de todo el código repetitivo desordenado.

Utilización del monitor evento descrito en mi artículo, las pruebas se puede escribir así:

var publisher = new AsyncEventPublisher(); 

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

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

EventMonitor.Assert(publisher, test, expectedSequence); 

O para un tipo que implementa INotifyPropertyChanged:

var publisher = new PropertyChangedEventPublisher(); 

Action test =() => 
{ 
    publisher.X = 1; 
    publisher.Y = 2; 
}; 

var expectedSequence = new[] { "X", "Y" }; 

EventMonitor.Assert(publisher, test, expectedSequence); 

Y para el caso en la pregunta original :

MyClass myObject = new MyClass(); 
EventMonitor.Assert(myObject,() => { myObject.Width = 42; }, "Width"); 

EventMonitor hace todo el trabajo pesado y ejecutará la prueba (acción) y afirmar que los eventos se generan en la secuencia esperada (sequenceSequence). También imprime buenos mensajes de diagnóstico en caso de falla de prueba. Reflection e IL se usan debajo del capó para que funcione la suscripción a eventos dinámicos, pero todo esto está muy bien encapsulado, por lo que solo se requiere un código como el anterior para escribir pruebas de eventos.

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/

+0

¡Deslizante! Si eso no responde la pregunta, nada lo hace. – Thomas

+0

Hola Thomas, ejemplos de código actualizados después de volver a leer la pregunta original. Ajusté la API de EventMonitor para que sea un poco más concisa. Aclamaciones. :) –

+0

Muy bien, de hecho, pero ni siquiera se acerca a manejar "eventos arbitrarios" como has afirmado en tu blog.Sin embargo, probablemente el 99% de todos los eventos en la naturaleza sigan las recomendaciones de MS, por lo que la falta de generalidad debería ser un gran problema en la práctica. –

5

Con lambdas puede hacer esto con muy poco código. Simplemente asigne un lambda al evento y establezca un valor en el controlador. No hay necesidad de reflexión y ganas de tipo firme refactorización

[TestFixture] 
public class TestClass 
{ 
    [Test] 
    public void TestEventRaised() 
    { 
     // arrange 
     var called = false; 

     var test = new ObjectUnderTest(); 
     test.WidthChanged += (sender, args) => called = true; 

     // act 
     test.Width = 42; 

     // assert 
     Assert.IsTrue(called); 
    } 

    private class ObjectUnderTest 
    { 
     private int _width; 
     public event EventHandler WidthChanged; 

     public int Width 
     { 
      get { return _width; } 
      set 
      { 
       _width = value; OnWidthChanged(); 
      } 
     } 

     private void OnWidthChanged() 
     { 
      var handler = WidthChanged; 
      if (handler != null) 
       handler(this, EventArgs.Empty); 
     } 
    } 
} 
+0

Yah, lo sé. Pero todavía hay 4 líneas donde 1 debería ser suficiente. Solo estoy tratando de ver si puedo hacerlo de manera más general. – Thomas

+2

+1 esta es una solución poco apreciada. Es fácil de leer, comprender y depurar. Evita las dependencias de código complicado (MSIL, Reflection, etc.) y puede cubrir casi todos los casos extremos de controladores de eventos personalizados, o lo que sea. Debería votarse más alto al menos. – HodlDwon

2

una solución en el estilo que usted propone que abarca todos los casos serán extremadamente difíciles de implementar. Pero si está dispuesto a aceptar que los tipos de delegado con los parámetros ref y out o los valores devueltos no se cubrirán, debería poder usar un DynamicMethod.

En el momento del diseño, cree una clase para contestar, llamemos a CallCounter.

En AssertRaisesEvent:

  • crear una instancia de su CallCounterclass, manteniéndolo en una inflexible de tipos variables
  • inicializar el contador a cero
  • construct a DynamicMethod en su clase de contador

    new DynamicMethod(string.Empty, typeof(void), parameter types extracted from the eventInfo, typeof(CallCounter))

  • obtener el Dynami MethodBuilder y de cMethod utilizar Reflection.Emit añadir los códigos de operación para incrementar el campo

    • ldarg.0 (el puntero this)
    • ldc_I4_1 (una constante)
    • ldarg.0 (el puntero this)
    • ldfld (leer el valor actual de la cuenta)
    • añadir
    • stfld (poner el recuento actualizado de nuevo en la variable miembro)
  • llamada the two-parameter overload of CreateDelegate, primer parámetro es el tipo de evento tomado de eventInfo, segundo parámetro es su instancia de CallCounter
  • pase el delegado resultante a eventInfo.AddEventHandler (tienes esto) Ahora estás listo para ejecutar el caso de prueba (ya tienes esto).
  • finalmente leyó el recuento de la forma habitual.

El único paso que no estoy 100% seguro de cómo lo haría es obtener los tipos de parámetros de EventInfo. ¿Usaste el EventHandlerType property y luego? Bueno, hay un ejemplo en esa página que muestra que simplemente tomas MethodInfo para el método Invoke del delegado (supongo que el nombre "Invoke" está garantizado en algún lugar del estándar) y luego GetParameters y luego sacas todos los valores de ParameterType, verificando que no hay parámetros de ref/out en el camino.

+0

Estoy seguro de que Marc Gravell creará una forma de usar árboles de expresión para evitar el MethodBuilder, pero sería más o menos la misma idea. Y obtener la Expresión correcta .Compile, cuando TDelegate no se conoce en tiempo de compilación, no es trivial. –

+0

Si esta es realmente la única manera de hacerlo, creo que me conformaré con la plantilla repetitiva ... – Thomas

1

¿Qué tal esto:

private void AssertRaisesEvent(Action action, object obj, string eventName) 
    { 
     EventInfo eventInfo = obj.GetType().GetEvent(eventName); 
     int raisedCount = 0; 
     EventHandler handler = new EventHandler((sender, eventArgs) => { ++raisedCount; }); 
     eventInfo.AddEventHandler(obj, handler); 
     action.Invoke(); 
     eventInfo.RemoveEventHandler(obj, handler); 

     Assert.AreEqual(1, raisedCount); 
    } 
+0

Esto solo funciona si el evento declarado es de tipo 'EventHandler', no, por ejemplo,' PropertyChangedEvent' o incluso un tipo de evento personalizado. – Thomas

Cuestiones relacionadas