如何对来自 ReactiveCommand (ReactiveUI) 的异常进行单元测试?

How to unit test exceptions from ReactiveCommand (ReactiveUI)?

我正在使用 xUnit 和 ReactiveUI 11.2(对于 WPF、.NET Framework 4.8,但我认为我的问题更通用)。

基本上,我想在 ViewModels 中测试我的 ReactiveCommand

例如,有一些情况在我的代码中抛出异常,我的程序崩溃了。

我想做一个单元测试来重现这个错误(单元测试应该会失败),然后我会修复我的错误,以某种方式防止异常,然后我的测试应该通过以反映修复。 (相当标准的程序)。

问题是,在 ReactiveCommand 期间抛出的任何异常似乎都是 ReactiveUI "swallowed",异常不会使测试失败

此外,如果我尝试在 .Subscribe() 的回调中编写 Assert() 语句,也会发生同样的情况:我可以在调试期间看到我的断言正确失败,但测试标记为绿色"passed" 无论如何。

我尝试了一些不同的方式来尝试使用调度程序,但没有任何改进。 我尝试按照描述使用“.ThrownExceptions”也无济于事。

这里有一些文档:https://reactiveui.net/docs/handbook/testing/


TL;DR

如何在我的 ReactiveCommand 中设置异常导致我的单元测试失败?我应该如何对 ReactiveCommand 进行完整的单元测试?


下面是演示该问题的完整程序。

与 NuGet 包一起使用: xunit 2.4.1, xunit.runner.visualstudio 2.4.1, ReactiveUI.Testing11.2.1

using Microsoft.Reactive.Testing;
using ReactiveUI;
using ReactiveUI.Testing;
using System;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using Xunit;

namespace Tests
{
    public class Foo
    {

        public ReactiveCommand<Unit, Unit> TestCommand { get; }

        public Foo(IScheduler? scheduler = null)
        {
            scheduler ??= RxApp.MainThreadScheduler;

            TestCommand = ReactiveCommand.Create(Explode, canExecute: null, outputScheduler: scheduler);
        }

        public void Explode()
        {
            throw new Exception("Boom");
        }
    }

    public class ReactiveCommandTests
    {

        // Should fail? (it doesn't fail)
        [Fact]
        public void Test1()
        {
            var foo = new Foo();
            foo.TestCommand.Execute().Subscribe();
        }

        // Should fail (it fails alright ! no ReactiveUI Observable here...)
        [Fact]
        public void Test2()
        {
            var foo = new Foo();
            foo.Explode();
        }

        // Should fail? (it doesn't fail)
        [Fact]
        public void Test3()
        {
            var testScheduler = new TestScheduler();
            var foo = new Foo(testScheduler);
            foo.TestCommand.Execute().Subscribe();
        }

        // Should fail? (it doesn't fail)
        [Fact]
        public void Test4()
        {
            new TestScheduler().With(scheduler =>
            {
                var foo = new Foo(scheduler);
                foo.TestCommand.Execute().Subscribe();

            });
        }

        // Should fail ? (it doesn't fail)
        [Fact]
        public void Test5()
        {
            var foo = new Foo();
            foo.TestCommand.ThrownExceptions.Subscribe(
                (ex) => {
                    Console.WriteLine("Exception detected !");
                    Assert.False(true); // This is hit, but doesn't even make the test fail....
                });

            foo.TestCommand.Execute().Subscribe();
        }
    }
}

所有测试都会导致抛出异常,所有测试都应该在 IMO 中失败,但只有 未使用 `ReactiveCommand 失败。

@Pac0,试试这个。您需要添加 FluentAssertions 包。我也建议定义自定义异常。

// Arrange
var foo = new Foo();            

// Act
var result = foo.TestCommand.Execute().Subscribe();

// Assert
result.Should().BeOfType<Exception>();

经过一些尝试以及此文档的帮助:http://introtorx.com/Content/v1.0.10621.0/16_TestingRx.html,我终于找到了丢失的东西。

一个需要:

  • 使用 TestScheduler
  • 并告诉测试调度程序运行(这将立即执行可观察对象中的所有内容)

所以问题中 Test3 的这种改编似乎工作正常:

[Fact]
public void Test3()
{
    var testScheduler = new TestScheduler();
    var foo = new Foo(testScheduler);
    foo.TestCommand.Execute().Subscribe();
    testScheduler.Start(); // YEAY
}

总的来说,我们最终将所有使用 ReactiveCommands 的测试包装在 With 块中:

new TestScheduler().With(scheduler =>
{
    var testScheduler = new TestScheduler();
    var foo = new Foo(testScheduler);
    foo.TestCommand.Execute().Subscribe();
    testScheduler.Start(); // YEAY
});

导致测试失败,堆栈跟踪显示它是由异常引起的:

 Tests.ReactiveCommandTests.Test3
   Source: ReactiveCommandTests.cs line 50
   Duration: 45 ms

  Message: 
    System.Exception : Boom
  Stack Trace: 
    Foo.Explode() line 26
    <>c__DisplayClass0_0.<Create>b__1(IObserver`1 observer) line 108
    CreateWithDisposableObservable`1.SubscribeCore(IObserver`1 observer) line 35
    ObservableBase`1.Subscribe(IObserver`1 observer) line 58
    --- End of stack trace from previous location where exception was thrown ---
    ExceptionDispatchInfo.Throw()
    <.cctor>b__2_1(Exception ex) line 16
    AnonymousSafeObserver`1.OnError(Exception error) line 62
    ObserveOnObserverNew`1.DrainStep(ConcurrentQueue`1 q) line 553
    ObserveOnObserverNew`1.DrainShortRunning(IScheduler recursiveScheduler) line 509
    <>c__DisplayClass4_0`1.<ScheduleAbsolute>b__0(IScheduler scheduler, TState state1) line 430
    ScheduledItem`1.Invoke() line 44
    VirtualTimeSchedulerBase`2.Start() line 174
    ReactiveCommandTests.Test3() line 56