简体   繁体   English

使用Rx.NET测试IObservable在特定时间内是否发出值

[英]Testing whether an IObservable emitted or didn't emit a value within a certain period of time with Rx.NET

I have a program that I'm currently refactoring to use the Membus message bus for event aggregation, and this message bus allows me to "observe" events on the bus by returning instances of IObservable that I can subscribe to. 我有一个程序,目前正在重构,以使用Membus消息总线进行事件聚合,而该消息总线使我可以通过返回我可以订阅的IObservable实例来“观察”总线上的事件。

In my unit tests, I want to ensure that my components only publish particular messages on the bus when it's appropriate. 在单元测试中,我想确保我的组件仅在适当时才在总线上发布特定消息。 The way I've tried to do this is by including the following sort of setup logic in my test specification classes: 我尝试执行此操作的方法是在我的测试规范类中包括以下类型的设置逻辑:

private readonly IBus messageBus;
private readonly IObservable<Model> myObservable;

public ComponentModelGatewaySpec()
{
    messageBus = TestHelper.DefaultMessageBus;
    myObservable = messageBus.Observe<ModelPublishedEventMessage>().Select(it => it.Model);
}

Then in a test case, I'd like to do something like the following: 然后在一个测试用例中,我想做如下事情:

public async Task Only_Publish_Incomplete_Models_After_Receiving_Request()
{
    var defaultTimeout = TimeSpan.FromMilliseconds(1000);

    // GIVEN a component model gateway and an incomplete model update.
    var modelUpdate = new Model { IntProperty = 1, BoolProperty = null };
    Assert.False(modelUpdate.IsComplete);
    var gateway = MockComponentModelGateway;
    gateway.SetMessageBus(messageBus);

    // EXPECT no current model is published after publishing the incomplete model update.
    Task<bool> noModelPublished = myObservable.WithinWindow(defaultTimeout).NoEmissionsOccurred().ToTask();
    messageBus.Publish(new ModelUpdateEventMessage(modelUpdate));
    Assert.True(await noModelPublished);

    // WHEN we publish a current model query.
    Task<Model> publishedModel = myObservable.WithinWindow(defaultTimeout).FirstAsync().ToTask();
    messageBus.Publish(new ModelQueryRequestedEventMessage());

    // THEN the model should be published.
    Assert.Equal(modelUpdate, await publishedModel);
}

What I'm essentially after is a way of testing either: 我所需要的基本上是一种测试方法:

  • "No events of this particular type were published after I performed this (series of) action(s), at least within X amount of time." “至少在X时间内,我执行此(一系列)操作后,没有发布任何特定类型的事件。”
  • "An event of this particular type was published after I performed this (series of) action(s), and this is what I expect the properties of that event to be." “执行此(一系列)操作后,将发布此特定类型的事件,这是我期望该事件的属性。”

I'd like to be able to handle all this logic asynchronously, or else I'll have a bunch of test cases that end up blocking for 1 or more seconds. 我希望能够异步处理所有这些逻辑,否则我将拥有大量的测试用例,最终会阻塞1秒钟或更长时间。

It may be possible to use Timeout for this, but Timeout causes an exception to be thrown on timeouts which seems like a cludgy way of handling things when I'm expecting them to be thrown. 也许可以使用Timeout ,但是Timeout会导致在超时上引发异常,这在我希望抛出异常时似乎是一种笨拙的处理方式。 I do use Timeout in observable compositions, but only in cases where a timeout occurring means the test should fail. 我确实在可观察的合成中使用了Timeout ,但是仅在发生超时意味着测试应该失败的情况下使用。

Currently, I'm trying to use various combinations of Window , Buffer , FirstAsync , etc. to accomplish this but I'm not getting the behaviour I expect in all of my test cases. 当前,我正在尝试使用WindowBufferFirstAsync等的各种组合来完成此操作,但是在所有测试用例中我都没有得到期望的行为。

Edit 编辑

I've added my own solution but I'm treating it as a temporary measure until I can incorporate Lee Campbell's advice (see his answer below). 我已经添加了自己的解决方案,但在将李·坎贝尔的建议纳入我的建议之前,我将其作为一种临时措施(请参阅下面的回答)。

You want to avoid concurrency (well multi threading) in your unit tests if you can. 如果可以的话,您希望在单元测试中避免并发(多线程)。 Concurrent unit tests can be non-deterministic and also run a lot slower ie have to run in real time. 并发单元测试可能是不确定的,而且运行速度也慢得多,即必须实时运行。 For example if you are trying to prove a timeout of 10seconds will throw an error, you will have to have your test run for 10 seconds. 例如,如果您尝试证明10秒的超时将引发错误,则必须将测试运行10秒。 This is not a scalable practice. 这不是可扩展的做法。

Instead consider using the TestScheduler . 而是考虑使用TestScheduler This will mean that you will need to have seams where you can provide schedulers to your operators. 这意味着您将需要接缝 ,以便可以向操作员提供调度程序。 Hopefully the API that is exposing these Observable sequences is friendly to testing. 希望公开这些Observable序列的API对测试友好。

public async Task Only_Publish_Incomplete_Models_After_Receiving_Request()
{
    var gateway = MockComponentModelGateway;
    gateway.SetMessageBus(messageBus);


    var defaultTimeout = TimeSpan.FromMilliseconds(1000);
    var scheduler = new TestScheduler();


    // GIVEN a component model gateway and an incomplete model update.
    var modelUpdate = new Model { IntProperty = 1, BoolProperty = null };
    Assert.False(modelUpdate.IsComplete);

    scheduler.Schedule(TimeSpan.FromMilliseconds(100),() => {
        messageBus.Publish(new ModelUpdateEventMessage(modelUpdate));
    });

    scheduler.Schedule(TimeSpan.FromMilliseconds(200),() =>
    {
        messageBus.Publish(new ModelQueryRequestedEventMessage());
    });

    var observer = scheduler.CreateObserver<Model>();

    myObservable.Subscribe(observer);

    scheduler.Start();

    CollectionAssert.AreEqual(
        new[]{
            ReactiveTest.OnNext(TimeSpan.FromMilliseconds(200).Ticks, modelUpdate)
        },
        observer.Messages);
}

Here you dont have to test for absence ( Assert.True(await noModelPublished); ), because you can see in the output that the value is not pushed until the point in virtual time (200ms) that the messageBus.Publish(new ModelQueryRequestedEventMessage()); 在这里,您不必测试是否缺少( Assert.True(await noModelPublished); ),因为您可以在输出中看到直到虚拟时间(200ms)内messageBus.Publish(new ModelQueryRequestedEventMessage()); was executed. 被执行了。

Now your tests should run synchronously but be able to verify an otherwise async flow. 现在,您的测试应该同步运行,但是能够验证否则会异步的流。

Let me preface by saying that I developed this solution before acting on Lee Campbell's advice, and a solution based on his advice would probably be a lot better since he (literally) wrote the book on the subject. 首先,请允许我说,我是在根据Lee Campbell的建议采取行动之前开发了此解决方案的,而基于他的建议的解决方案可能会更好,因为他(从字面上看)写了关于该主题的书。 That said, the solution I came up with works well enough for my specific use case. 就是说,我想出的解决方案足以满足我的特定用例。

Using the same example test case that I used in the original post, I now have this instead: 使用我在原始帖子中使用的相同示例测试用例,现在改为使用:

[Theory]
[PairwiseData]
public async Task Adapted_Component_Model_Gateway_Should_Publish_Current_Model_When_Requested(
    [CombinatorialValues(null, 1, 2)] int? intValue,
    [CombinatorialValues(null, true, false)] bool? boolValue)
{
    var model = new AllowStaleDetailsMockModel { IntProperty = intValue, BoolProperty = boolValue };
    if (model.IsComplete) return;

    // GIVEN an initialized adapted component model gateway and a given _INCOMPLETE_ current model.
    var adaptedGateway = AdaptGateway(MockComponentModelGateway);
    adaptedGateway.SetMessageBus(messageBus);
    adaptedGateway.Initialize();

    // EXPECT no current model is published after publishing the incomplete model update.
    var messagePublished = allowStaleCurrentModelObservable.BufferEmissions().ContainsEvents();
    messageBus.Publish(new CurrentModelUpdateReadyEventArgs<AllowStaleDetailsMockModel>(model));
    Assert.False(await messagePublished);

    // WHEN we publish a current model query.
    var actualModel = allowStaleCurrentModelObservable.WaitForEmission();
    messageBus.Publish(new CurrentModelQueryRequestedEventArgs());

    // THEN the current model should be published.
    Assert.Equal(model, await actualModel);
}

In a "test utility" class, I created the following: 在“测试实用程序”类中,我创建了以下内容:

public static class TestHelper
{
    public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMilliseconds(1000);

    public static IBus DefaultMessageBus
    {
        get
        {
            return BusSetup.StartWith<Conservative>().Construct();
        }
    }

    public static async Task<bool> ContainsEvents<T>(this Task<IList<T>> eventBufferTask)
    {
        return (await eventBufferTask).Any();
    }
}

public static class ObservableExtensions
{
    public static Task<T> WaitForEmission<T>(this IObservable<T> observable)
    {
        return observable.WaitForEmission(TestHelper.DefaultTimeout);
    }

    public static Task<T> WaitForEmission<T>(this IObservable<T> observable, TimeSpan timeout)
    {
        return observable.FirstAsync().Timeout(timeout).ToTask();
    }

    public static Task<IList<T>> BufferEmissions<T>(this IObservable<T> observable)
    {
        return observable.BufferEmissions(TestHelper.DefaultTimeout);
    }

    public static Task<IList<T>> BufferEmissions<T>(this IObservable<T> observable, TimeSpan bufferWindow)
    {
        return observable.Buffer(bufferWindow).FirstAsync().ToTask();
    }
}

I use Observable.WaitForEmissions in test cases right before doing something that I expect should publish a particular kind of message. 我在测试用例中使用了Observable.WaitForEmissions ,然后才执行我希望发布某种特定消息的操作。 This returns a task that will either: 这将返回一个任务,该任务将:

  • return a single value emitted by the observable 返回可观察对象发出的单个值
  • throw a timeout error if no emission was detected in a given length of time (by default, 1 second). 如果在给定的时间长度(默认为1秒)内未检测到发射,则抛出超时错误。

I use Observable.BufferEmissions in test cases where I'm either expecting multiple values to be published and I want to collect them all, or I want to check whether values were published in a given length of time or not without catching TimeoutException errors ( Task<IList<T>>.ContainsEvents works great for this). 我在测试用例中使用Observable.BufferEmissions ,这些测试用例是希望发布多个值并希望收集所有值,或者我想检查值是否在给定的时间内发布,而不捕获TimeoutException错误( Task<IList<T>>.ContainsEvents为此非常Task<IList<T>>.ContainsEvents )。

All of the test cases in my project perform as expected, and my ~600 odd test cases are discovered and executed in about 30 seconds, which I'm happy enough with. 我项目中的所有测试用例均按预期方式运行,大约30秒内发现并执行了约600个奇怪的测试用例,我对此感到非常满意。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM