2010-10-12 16 views
6

J'utilise Reactive Extensions for .NET (Rx) pour exposer des événements sous la forme IObservable<T>. Je veux créer un test unitaire où j'affirme qu'un événement particulier est déclenché. Voici une version simplifiée de la classe que je veux tester:Test d'unité pour un événement à l'aide d'extensions réactives

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

} 

Évidemment, mes vraies classes sont plus complexes. Mon but est de vérifier que l'exécution de certaines actions avec la classe testée conduit à une séquence d'événements signalés sur le IObservable. Heureusement, les classes que je veux tester implémentent IDisposable et l'appel OnCompleted sur le sujet lorsque l'objet est disposé rend beaucoup plus facile à tester.

Voici comment je teste:

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

// Act 
classUnderTest.DoSomething(); 

// Assert 
Assert.IsTrue(eventFired); 

En utilisant une variable pour déterminer si un événement est déclenché est pas trop mal, mais dans des scénarios plus complexes je veux vérifier qu'une séquence particulière d'événements sont mis à la porte. Est-ce possible sans simplement enregistrer les événements dans les variables et ensuite faire des assertions sur les variables? La possibilité d'utiliser une syntaxe proche de LINQ pour faire des assertions sur un IObservable rendrait le test plus lisible.

+1

BTW, je pense avoir une variable est parfaitement bien. Le code ci-dessus est facile à lire, ce qui est le plus important. La réponse du @ PL est belle et élégante, mais vous devez essayer de comprendre ce qui se passe ... Peut-être le transformer en extension FailIfNothingHappened() –

+0

@Sergey Aldoukhov: Je suis d'accord, mais la réponse de PL m'a appris comment utiliser ' Matérialiser »pour raisonner sur le comportement de mon« IObservable ». Et pour des tests plus complexes utilisant des variables pour capturer ce qui s'est passé, il peut être plus difficile à comprendre. En outre, créer une extension comme vous le suggérez facilitera probablement la compréhension de ce qui se passe. –

+0

J'ai modifié ma question pour clarifier ce que je veux. –

Répondre

11

Cette réponse a été mise à jour vers la version 1.0 maintenant publiée de Rx.

La documentation officielle est encore rare mais Testing and Debugging Observable Sequences sur MSDN est un bon point de départ.

La classe de test doit dériver de ReactiveTest dans l'espace de noms Microsoft.Reactive.Testing. Le test est basé sur un TestScheduler qui fournit un temps virtuel pour le test.

La méthode TestScheduler.Schedule peut être utilisée pour mettre en file d'attente des activités en certains points (ticks) en temps virtuel. Le test est exécuté par TestScheduler.Start. Cela renverra un ITestableObserver<T> qui peut être utilisé pour affirmer par exemple en utilisant 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 est utilisé pour programmer un appel à DoSomething la fin du temps 20 (mesuré dans les tiques).

Ensuite TestScheduler.Start est utilisé pour effectuer le test réel sur le SomethingHappened observable. La durée de vie de l'abonnement est contrôlée par les arguments de l'appel (mesuré à nouveau en ticks).

Enfin ReactiveAssert.AreElementsEqual est utilisé pour vérifier que OnNext a été appelée à l'heure 20 comme prévu.

Le test vérifie que l'appel DoSomething déclenche immédiatement l'observable SomethingHappened.

0

Je ne suis pas sûr de parler plus couramment mais cela fera l'affaire sans introduire de 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

Je suis à peu près sûr que cela ne fonctionnera pas, Affirmer dans un abonnement se traduit souvent par des choses bizarres qui se passent et le test passe toujours. –

+0

Pour que le test échoue, vous devez commenter la ligne avec l'appel OnNext. –

+1

Juste un point; AsObservable() ne sert à rien dans cet exemple. –

3

Ce type de test pour les observables serait incomplet. Tout récemment, l'équipe RX a publié le planificateur de test et certaines extensions (que BTW utilise en interne pour tester la bibliothèque). En utilisant ceux-ci, vous pouvez non seulement vérifier si quelque chose s'est passé ou non, mais aussi vous assurer que la synchronisation et l'ordre sont corrects. En prime, le planificateur de test vous permet d'exécuter vos tests en "temps virtuel", de sorte que les tests sont exécutés instantanément, quel que soit le délai que vous utilisez.

Jeffrey van Gogh de l'équipe RX published an article on how to do such kind of testing.

Le test ci-dessus, en utilisant l'approche mentionnée, ressemblera à ceci:

[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: Vous pouvez également appeler .OnNext (ou une autre méthode) implicitement:

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

Mon point est - dans les situations les plus simples, il vous suffit de vous assurer que l'événement est déclenché (fe vous vérifiez votre Où travaille c ourectement). Mais alors que vous progressez dans la complexité, vous commencez à tester le timing, ou l'achèvement, ou quelque chose d'autre qui nécessite le planificateur virtuel. Mais la nature du test utilisant l'ordonnanceur virtuel, opposé aux tests "normaux", est de tester l'ensemble des opérations observables en même temps, et non "atomiques".

Vous devriez donc probablement passer au planificateur virtuel quelque part - pourquoi ne pas commencer par le début?

P.S. En outre, vous devrez recourir à une logique différente pour chaque cas de test - f.e. vous auriez des observations très différentes pour tester que quelque chose ne s'est pas passé en face de ce qui s'est passé.

+2

Cette approche semble très appropriée pour tester quelque chose comme Rx lui-même. Cependant, je ne souhaite pas synthétiser les appels 'OnNext'. Au lieu de cela, je veux affirmer qu'un appel à la méthode dans la classe que je teste conduit en fait à un appel à 'OnNext' sur un' IObservable'. –