使用反应式扩展对事件进行单元测试

问题描述 投票:0回答:4

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

c# .net unit-testing system.reactive
4个回答
16
投票

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

官方文档仍然很少,但 MSDN 上的测试和调试可观察序列是一个很好的起点。

测试类应派生自 ReactiveTest

 命名空间中的 
Microsoft.Reactive.Testing
。该测试基于 
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
进行实际测试。订阅的生命周期由调用的参数控制(同样以刻度为单位)。

最后

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

测试验证调用

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 是否正常工作)。但随着复杂性的提高,您开始测试时间、完成度或其他需要虚拟调度程序的东西。但是使用虚拟调度程序的测试的本质与“正常”测试相反,是立即测试整个可观察的,而不是“原子”操作。

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

附注此外,您必须对每个测试用例采用不同的逻辑 - 例如您将有非常不同的可观察值来测试未发生的事情与发生的事情相反。


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

0
投票

也许像这样?而且很容易阅读。

[Fact]
    public async Task MyTest()
    {
        await new TestScheduler().WithAsync(async scheduler =>
        {
            // Arrange
            var sut = new MyTestClass();
            var observer = scheduler.CreateObserver<bool>();
            sut.EnableLaser.CanExecute.Subscribe(observer);

            // Act
            sut.EnableLaser.Execute().Subscribe();

            // Assert
            observer.Messages.Should().HaveCount(2);
            observer.Messages.First().Value.Value.Should().BeTrue();
            observer.Messages.Last().Value.Value.Should().BeFalse();
        });
    }
© www.soinside.com 2019 - 2024. All rights reserved.