使用响应式扩展进行事件的单元测试

12

我正在使用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()。 - Sergey Aldoukhov
@Sergey Aldoukhov: 我同意,但 PL 的回答让我学会了如何使用 Materialize 来理解我的 IObservable 行为。对于更复杂的测试,使用变量来捕获发生的事情可能更难理解。此外,像您建议的创建扩展可能会更容易理解正在发生什么。 - Martin Liversage
我已经编辑了我的问题,以使我的意图更加清晰明确。 - Martin Liversage
3个回答

16

这个答案已经更新到了最新版本1.0的Rx。

官方文档仍然很少,但Microsoft官方网站上的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(以tick为单位)安排对DoSomething的调用。

然后,TestScheduler.Start 用于对可观察对象 SomethingHappened 进行实际测试。 订阅的生命周期由调用的参数来控制(同样以tick为单位)。

最后,ReactiveAssert.AreElementsEqual 被用于验证预期的时间点20时是否已调用OnNext

这个测试验证了立即调用DoSomething会触发可观测对象SomethingHappened


8
这种针对可观察对象的测试是不完整的。最近,RX团队发布了测试调度程序和一些扩展(顺便说一句,他们在测试库时也使用这些扩展)。使用它们,您不仅可以检查是否发生了某些事情,还可以确保时间和顺序是正确的。作为奖励,测试调度程序允许您在“虚拟时间”中运行测试,因此无论您在其中使用多大的延迟,测试都会立即运行。
RX团队的Jeffrey van Gogh发表了一篇关于如何进行这种类型的测试的文章。 使用上述方法进行的测试将如下所示:
[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 是否正常工作)。但是随着复杂度的增加,你开始测试时间、完成情况或其他需要虚拟调度程序的内容。但是使用虚拟调度程序进行测试的性质与“普通”测试相反,它是一次性测试整个可观察对象,而不是“原子”操作。
因此,也许你需要在某个时候切换到虚拟调度程序 - 为什么不从一开始就开始呢?
另外,每个测试用例都需要采用不同的逻辑 - 比如,你需要非常不同的可观察对象来测试某些事情没有发生,与测试某些事情发生相对应。

2
这种方法似乎非常适合测试 Rx 本身。但是,我不想合成 OnNext 调用。相反,我想断言我正在测试的类中的方法调用实际上会导致对 IObservableOnNext 调用。 - Martin Liversage
你使用的是哪个 System.Reactive 版本?它不包含 CreateHotObservable。 - virtouso

1

不确定是否更流畅,但这样做可以避免引入变量。

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
我相信这个不会起作用,在订阅中使用Assert通常会导致一些奇怪的事情发生,但测试仍然通过。 - Ana Betts
为了使测试失败,您应该注释掉带有OnNext调用的那一行。 - PL.
1
仅仅是一个提示;在这个示例中,AsObservable() 没有任何作用。 - Lee Campbell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接