2010-10-12 60 views
6

我使用Reactive Extensions for .NET (Rx)将事件公开为IObservable<T>。我想创建一个单元测试,我声称发生了一个特定的事件。这里是我想测试的类的简化版本:使用反应性扩展对事件进行单元测试

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

} 

显然我的真实类更复杂。我的目标是验证在被测试类中执行某些操作会导致在IObservable上发出一系列事件。幸运的是,我想要测试的课程实现IDisposable,并且在对象丢弃时调用OnCompleted就可以更容易测试。

这里是我如何测试:

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

// Act 
classUnderTest.DoSomething(); 

// Assert 
Assert.IsTrue(eventFired); 

使用一个变量来确定是否触发的事件是不是太糟糕,但在更复杂的情况我可能要验证事件的特定顺序被解雇。如果没有简单地将事件记录在变量中,然后对变量进行断言,这是否可能?能够使用类似LINQ的流利语法来对IObservable进行断言将有希望使测试更具可读性。

+1

顺便说一句,我认为有一个变量是非常好的。上面的代码很容易阅读,这是最重要的。 @ PL的答案很好,也很优雅,但是你必须努力去理解正在发生什么......也许把它变成一个扩展FailIfNothingHappened() – 2010-10-15 15:33:56

+0

@Sergey Aldoukhov:我同意,但是PL的答案学会了我如何使用'物化'来推测我的IObservable行为。对于使用变量来捕捉发生的事情的更复杂的测试可能更难理解。另外,根据您的建议创建扩展可能会更容易理解正在发生的事情。 – 2010-10-15 16:01:32

+0

我编辑了我的问题,使其更清楚我想要什么。 – 2010-10-29 11:59:03

回答

11

此答案已更新至现在发布的Rx版本1.0。

官方文档仍然不足,但MSDN上的Testing and Debugging Observable Sequences是一个很好的起始位置。

测试类别应该来自Microsoft.Reactive.Testing命名空间中的ReactiveTest。该测试基于为测试提供虚拟时间的TestScheduler

TestScheduler.Schedule方法可用于在虚拟时间的某些点(滴答)排队活动。测试由TestScheduler.Start执行。这将返回一个ITestableObserver<T>,可用于断言,例如使用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用于调度在时间20(在蜱测量)到DoSomething的呼叫。

然后TestScheduler.Start用于在可观察的SomethingHappened上执行实际测试。订阅的生命周期由调用的参数控制(再次以tick为单位)。

最后ReactiveAssert.AreElementsEqual用于验证OnNext在预期的时间20被调用。

该测试验证呼叫DoSomething立即触发可观察的SomethingHappened

0

不确定更流利,但这不会引入变量。

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

我相当肯定这不起作用,在订阅中声明经常导致奇怪的事情发生,并且测试仍然通过。 – 2010-10-13 17:29:06

+0

要测试失败,您应该使用OnNext调用注释掉该行。 – 2010-10-13 17:39:47

+1

只是一点;在这个例子中,AsObservable()没有任何用处。 – 2012-06-17 07:31:45

3

这种观察对象的测试是不完整的。就在最近,RX团队发布了测试调度程序和一些扩展(它们在内部用于测试库)。 使用这些,你不仅可以检查是否发生了某些事情,还可以确保时间和订单是正确的。作为奖励,测试计划程序允许您在“虚拟时间”内运行测试,因此无论您在内部使用多大的延迟,测试都能立即运行。

杰弗里·梵高从RX队published an article on how to do such kind of testing.

以上测试,使用上述方法,将是这样的:

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

编辑:您也可以拨打.OnNext(或其他方式)含蓄:

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

我的观点是 - 在最简单的情况下,你只需要确保该事件被触发(FE您检查您在哪里运行的C orrectly)。但随着您复杂性的进步,您将开始测试计时,完成或需要虚拟调度程序的其他内容。但是,与“正常”测试相反,使用虚拟调度程序进行测试的本质是一次测试整个观察值,而不是“原子”操作。

因此,您可能需要切换到虚拟调度程序的某个地方 - 为什么不从头开始?

P.S.此外,您将不得不为每个测试用例采取不同的逻辑 - f.e.你会有非常不同的观察结果,因为测试发现事情并没有发生与事情相反的事情。

+2

这种方法似乎非常适合测试Rx本身。但是,我不想综合'OnNext'调用。相反,我想断言在我正在测试的类中调用方法实际上是导致在“IObservable”上调用“OnNext”。 – 2010-10-15 09:40:12