2010-10-12 9 views
6

Sto usando Reactive Extensions for .NET (Rx) per esporre eventi come IObservable<T>. Voglio creare un test unitario in cui dichiaro che un particolare evento è stato licenziato. Ecco una versione semplificata della classe Voglio testare:Test di unità per un evento utilizzando le estensioni reattive

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

} 

Ovviamente i miei veri classi sono più complesse. Il mio obiettivo è verificare che l'esecuzione di alcune azioni con la classe sottoposta a test porti a una sequenza di eventi segnalati sullo IObservable. Fortunatamente le classi che voglio testare implementano IDisposable e chiamando OnCompleted sull'oggetto quando l'oggetto è disposto rende molto più facile testare.

ecco come lo prova:

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

// Act 
classUnderTest.DoSomething(); 

// Assert 
Assert.IsTrue(eventFired); 

Utilizzando una variabile per determinare se un evento viene generato non è male, ma in scenari più complessi mi può essere utile per verificare che una particolare sequenza di eventi sono licenziato. È possibile senza semplicemente registrare gli eventi nelle variabili e poi fare asserzioni sulle variabili? Poter usare una sintassi fluente simile a LINQ per fare asserzioni su un IObservable si spera che renderebbe il test più leggibile.

+1

BTW, penso che avere una variabile sia perfettamente buona. Il codice sopra è facile da leggere, il che è molto importante. La risposta di @ PL è bella ed elegante, ma devi sforzarti per capire cosa sta succedendo ... Forse trasformarlo in un'estensione FailIfNothingHappened() –

+0

@Sergey Aldoukhov: Sono d'accordo, ma la risposta di PL mi ha insegnato come posso usare ' Materializzare' per ragionare su come si comporta il mio 'IObservable'. E per i test più complessi che utilizzano le variabili per catturare ciò che è successo potrebbe essere più difficile da capire. Inoltre, la creazione di un'estensione come suggerito probabilmente renderà più facile capire cosa sta succedendo. –

+0

Ho modificato la mia domanda per rendere più chiaro ciò che voglio. –

risposta

11

Questa risposta è stata aggiornata alla versione 1.0 di Rx ora rilasciata.

La documentazione ufficiale è ancora scarsa ma lo Testing and Debugging Observable Sequences su MSDN è un buon punto di partenza.

La classe di test deve derivare da ReactiveTest nello spazio dei nomi Microsoft.Reactive.Testing. Il test si basa su un TestScheduler che fornisce il tempo virtuale per il test.

Il metodo TestScheduler.Schedule può essere utilizzato per accodare attività in determinati punti (zecche) in tempo virtuale. Il test viene eseguito da TestScheduler.Start. Ciò restituirà un ITestableObserver<T> che può essere utilizzato per asserire, ad esempio, utilizzando la classe 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 consente di pianificare una chiamata a DoSomething al tempo 20 (misurato in tick).

Quindi TestScheduler.Start viene utilizzato per eseguire il test effettivo sull'osservabile SomethingHappened. La durata della sottoscrizione è controllata dagli argomenti della chiamata (misurati di nuovo in tick).

Infine, ReactiveAssert.AreElementsEqual viene utilizzato per verificare che OnNext sia stato chiamato all'ora 20 come previsto.

Il test verifica che la chiamata DoSomething attiva immediatamente l'osservabile SomethingHappened.

0

Non sono sicuro di più fluente ma questo farà il trucco senza introdurre una variabile.

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

Sono abbastanza sicuro che questo non funzionerà, l'affermazione in un abbonamento spesso causa strane cose e il test continua a passare. –

+0

In caso di esito negativo del test, è necessario commentare la linea con la chiamata OnNext. –

+1

Solo un punto; AsObservable() non ha alcuno scopo in questo esempio. –

3

Questo tipo di test per osservabili sarebbe incompleto. Proprio di recente, il team RX ha pubblicato lo strumento di pianificazione dei test e alcune estensioni (che vengono utilizzate internamente per testare la libreria). Utilizzando questi, è possibile non solo verificare se qualcosa è accaduto o meno, ma anche assicurarsi che il tempo e l'ordine sia corretto. Come bonus, lo scheduler di test ti consente di eseguire i test in "tempo virtuale", in modo che i test vengano eseguiti immediatamente, indipendentemente dai ritardi che stai utilizzando all'interno.

Jeffrey van Gogh da squadra RX published an article on how to do such kind of testing.

La prova di cui sopra, utilizzando l'approccio citato, sarà simile a questa:

[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: È anche possibile chiamare .OnNext (o qualche altro metodo) implicitamente:

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

il mio punto è - nelle situazioni più semplici, è necessario solo per assicurarsi che l'evento viene generato (fe si sta controllando il vostro c Dove sta lavorando orrectly). Tuttavia, man mano che si procede con la complessità, si inizia a testare la tempistica, il completamento o qualcos'altro che richiede lo scheduler virtuale. Ma la natura del test che utilizza lo scheduler virtuale, opposto ai test "normali" è quella di testare l'intero osservabile in una volta, non operazioni "atomiche".

Quindi probabilmente dovresti passare allo scheduler virtuale da qualche parte lungo la strada - perché non iniziare con esso all'inizio?

P.S. Inoltre, dovresti ricorrere a una logica diversa per ogni caso di test - per esempio. avresti osservato molto diversamente per testare che qualcosa non è accaduto di fronte a qualcosa che è successo.

+2

Questo approccio sembra molto adatto per testare qualcosa come Rx stesso. Tuttavia, non voglio sintetizzare le chiamate 'OnNext'. Invece, voglio affermare che una chiamata al metodo nella classe che sto testando sta infatti portando a una chiamata a 'OnNext' su un' IObservable'. –

Problemi correlati