【问题标题】:Unit testing for an event using Reactive Extensions使用响应式扩展对事件进行单元测试
【发布时间】:2010-10-12 14:25:20
【问题描述】:

我使用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 进行断言有望使测试更具可读性。

【问题讨论】:

  • 顺便说一句,我认为有一个变量是非常好的。上面的代码很容易阅读,这是最重要的。 @PL 的答案很好很优雅,但你必须努力理解发生了什么......也许将其转换为扩展 FailIfNothingHappened()
  • @Sergey Aldoukhov:我同意,但是 PL 的回答让我知道了如何使用 Materialize 来推断我的 IObservable 的行为方式。而对于更复杂的测试,使用变量来捕捉所发生的事情可能更难理解。此外,按照您的建议创建一个扩展程序可能会更容易理解发生了什么。
  • 我已经编辑了我的问题,以便更清楚我想要什么。

标签: c# .net unit-testing system.reactive


【解决方案1】:

这个答案已经更新到现在发布的 Rx 1.0 版。

官方文档仍然很少,但 MSDN 上的 Testing and Debugging Observable Sequences 是一个很好的起点。

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

TestScheduler.Schedule 方法可用于在虚拟时间的特定点(滴答声)排队活动。测试由TestScheduler.Start 执行。这将返回一个ITestableObserver&lt;T&gt;,例如可以使用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进行实际测试。订阅的生命周期由调用的参数控制(同样以滴答声为单位)。

最后,ReactiveAssert.AreElementsEqual 用于验证 OnNext 在时间 20 是否按预期被调用。

测试验证调用DoSomething 会立即触发可观察的SomethingHappened

【讨论】:

    【解决方案2】:

    这种对 observables 的测试是不完整的。就在最近,RX 团队发布了测试调度程序和一些扩展(顺便说一句,他们在内部使用这些扩展来测试库)。 使用这些,您不仅可以检查是否发生了某事,还可以确保时间和顺序正确。作为奖励,测试调度程序允许您在“虚拟时间”中运行测试,因此无论您在内部使用多大的延迟,测试都会立即运行。

    RX 团队的 Jeffrey van Gogh 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()));
    

    我的意思是 - 在最简单的情况下,您只需要确保触发事件(例如,您正在检查您的 Where 是否正常工作)。但是随着复杂性的提高,您开始测试时间、完成或其他需要虚拟调度程序的东西。但是使用虚拟调度程序的测试与“正常”测试相反,其本质是一次测试整个可观察对象,而不是“原子”操作。

    因此,您可能不得不在以后的某个地方切换到虚拟调度程序 - 为什么不从一开始就开始呢?

    附:此外,您将不得不为每个测试用例诉诸不同的逻辑 - f.e.您将有非常不同的 observable 来测试与发生的事情相反的事情没有发生。

    【讨论】:

    • 这种方法似乎非常适合测试像 Rx 本身这样的东西。但是,我不想合成OnNext 调用。相反,我想断言,在我正在测试的类中调用方法实际上会导致在 IObservable 上调用 OnNext
    • 你使用了什么版本的 system.reactive?它不包含 CreateHotObservable
    【解决方案3】:

    不确定是否更流畅,但这会在不引入变量的情况下解决问题。

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

    【讨论】:

    • 我很确定这不会起作用,在订阅中断言通常会导致奇怪的事情发生并且测试仍然通过。
    • 如果测试失败,您应该使用 OnNext 调用注释掉该行。
    • 只是一点; AsObservable() 在这个例子中没有任何作用。
    猜你喜欢
    • 1970-01-01
    • 2019-02-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-07
    相关资源
    最近更新 更多