2010-10-12 12 views
6

Estoy usando Reactive Extensions for .NET (Rx) para exponer eventos como IObservable<T>. Quiero crear una prueba unitaria donde afirme que un evento particular se disparó. Aquí hay una versión simplificada de la clase que quiero probar:Pruebas unitarias para un evento usando Extensiones reactivas

public sealed class ClassUnderTest : IDisposable { 

    Subject<Unit> subject = new Subject<Unit>(); 

    public IObservable<Unit> SomethingHappened { 
    get { return this.subject.AsObservable(); } 
    } 

    public void DoSomething() { 
    this.subject.OnNext(new Unit()); 
    } 

    public void Dispose() { 
    this.subject.OnCompleted(); 
    } 

} 

Obviamente, mis clases reales son más complejas. Mi objetivo es verificar que realizar algunas acciones con la clase bajo prueba conduce a una secuencia de eventos señalados en el IObservable. Afortunadamente, las clases que deseo probar implementan IDisposable y llaman al OnCompleted sobre el tema cuando el objeto es eliminado hace que sea mucho más fácil de probar.

Aquí es cómo puedo probar:

// Arrange 
var classUnderTest = new ClassUnderTest(); 
var eventFired = false; 
classUnderTest.SomethingHappened.Subscribe(_ => eventFired = true); 

// Act 
classUnderTest.DoSomething(); 

// Assert 
Assert.IsTrue(eventFired); 

Usando una variable para determinar si un evento se dispara no es tan malo, pero en escenarios más complejos es posible que quiera verificar que una secuencia particular de los eventos están despedido. ¿Es eso posible sin simplemente registrar los eventos en variables y luego hacer aserciones sobre las variables? Sería bueno poder hacer la prueba para poder utilizar una sintaxis fluida de tipo LINQ para realizar afirmaciones en un IObservable.

+1

Por cierto, creo que tener una variable es perfectamente bueno. El código anterior es fácil de leer, que es lo más importante. La respuesta de @PL es agradable y elegante, pero tienes que esforzarte para entender lo que está pasando ... Tal vez transformarlo en una extensión FailIfNothingHappened() –

+0

@Sergey Aldoukhov: Estoy de acuerdo, pero la respuesta de PL me enseñó cómo puedo usar ' Materialise' para razonar sobre cómo se comporta mi 'IObservable'. Y para las pruebas más complejas que usan variables para capturar lo que sucedió puede ser más difícil de entender. Además, la creación de una extensión tal como sugiere probablemente le facilitará la comprensión de lo que está sucediendo. –

+0

He editado mi pregunta para que quede más claro lo que quiero. –

Respuesta

11

Esta respuesta se ha actualizado a la versión 1.0 de Rx.

La documentación oficial aún es escasa, pero Testing and Debugging Observable Sequences en MSDN es un buen punto de partida.

La clase de prueba debe derivar de ReactiveTest en el espacio de nombres Microsoft.Reactive.Testing. La prueba se basa en un TestScheduler que proporciona tiempo virtual para la prueba.

El método TestScheduler.Schedule se puede utilizar para poner en cola actividades en ciertos puntos (tics) en tiempo virtual. La prueba se ejecuta por TestScheduler.Start. Esto devolverá un ITestableObserver<T> que se puede usar para afirmar, por ejemplo, utilizando la clase ReactiveAssert.

public class Fixture : ReactiveTest { 

    public void SomethingHappenedTest() { 
    // Arrange 
    var scheduler = new TestScheduler(); 
    var classUnderTest = new ClassUnderTest(); 

    // Act 
    scheduler.Schedule(TimeSpan.FromTicks(20),() => classUnderTest.DoSomething()); 
    var actual = scheduler.Start(
    () => classUnderTest.SomethingHappened, 
     created: 0, 
     subscribed: 10, 
     disposed: 100 
    ); 

    // Assert 
    var expected = new[] { OnNext(20, new Unit()) }; 
    ReactiveAssert.AreElementsEqual(expected, actual.Messages); 
    } 

} 

TestScheduler.Schedule se usa para programar una llamada a DoSomething en el tiempo de 20 (medido en las garrapatas).

Luego TestScheduler.Start se utiliza para realizar la prueba real en el SomethingHappened observable. La duración de la suscripción está controlada por los argumentos de la llamada (medidos de nuevo en tics).

Finalmente, ReactiveAssert.AreElementsEqual se usa para verificar que OnNext se llamó en el momento 20 como se esperaba.

La prueba verifica que al llamar al DoSomething, se activa inmediatamente el SomethingHappened observable.

0

No estoy seguro acerca de la fluidez, pero esto hará el truco sin introducir una variable.

var subject = new Subject<Unit>(); 
subject 
    .AsObservable() 
    .Materialize() 
    .Take(1) 
    .Where(n => n.Kind == NotificationKind.OnCompleted) 
    .Subscribe(_ => Assert.Fail()); 

subject.OnNext(new Unit()); 
subject.OnCompleted(); 
+1

Estoy bastante seguro de que esto no funcionará. Afirmar en una suscripción a menudo resulta en cosas extrañas y la prueba sigue pasando. –

+0

Para que la prueba falle, debe comentar la línea con la llamada OnNext. –

+1

Solo un punto; AsObservable() no sirve para nada en este ejemplo. –

3

Este tipo de prueba de observables estaría incompleto. Recientemente, el equipo de RX publicó el programador de pruebas y algunas extensiones (que BTW usan internamente para probar la biblioteca). Utilizándolos, no solo puede verificar si algo sucedió o no, sino que también debe asegurarse de que la temporización y el pedido sean correctos. Como beneficio adicional, el planificador de pruebas le permite ejecutar sus pruebas en "tiempo virtual", por lo que las pruebas se ejecutan instantáneamente, sin importar qué tan grandes sean las demoras que use dentro.

Jeffrey Van Gogh del equipo de RX published an article on how to do such kind of testing.

La prueba anterior, utilizando el enfoque mencionado, se verá así:

[TestMethod] 
    public void SimpleTest() 
    { 
     var sched = new TestScheduler(); 
     var subject = new Subject<Unit>(); 
     var observable = subject.AsObservable(); 

     var o = sched.CreateHotObservable(
      OnNext(210, new Unit()) 
      ,OnCompleted<Unit>(250) 
      ); 
     var results = sched.Run(() => 
            { 
             o.Subscribe(subject); 
             return observable; 
            }); 
     results.AssertEqual(
      OnNext(210, new Unit()) 
      ,OnCompleted<Unit>(250) 
      ); 
    }: 

EDIT: También puede llamar .OnNext (o algún otro método) implícita:

 var o = sched.CreateHotObservable(OnNext(210, new Unit())); 
     var results = sched.Run(() => 
     { 
      o.Subscribe(_ => subject.OnNext(new Unit())); 
      return observable; 
     }); 
     results.AssertEqual(OnNext(210, new Unit())); 

Mi punto es - en situaciones más sencillas, que necesita sólo para asegurarse de que el evento se dispara (Fe se está comprobando su c Donde está trabajando orrectly). Pero a medida que avanza en complejidad, comienza a probar el tiempo, la finalización o cualquier otra cosa que requiera el programador virtual. Pero la naturaleza de la prueba que utiliza el planificador virtual, frente a las pruebas "normales" es probar todo lo observado a la vez, no las operaciones "atómicas".

Probablemente tendrías que cambiar al programador virtual en algún momento del camino. ¿Por qué no empezar desde el principio?

P.S. Además, tendrías que recurrir a una lógica diferente para cada caso de prueba, es decir, tendrías un observador muy diferente para probar que algo no sucedió frente a que algo sucedió.

+2

Este enfoque parece muy adecuado para probar algo como Rx. Sin embargo, no quiero sintetizar llamadas 'OnNext'. En su lugar, quiero afirmar que una llamada al método en la clase que estoy probando de hecho conduce a una llamada a 'OnNext' en un' IObservable'. –

Cuestiones relacionadas