如何对C# ReactiveX.IO开发的类编写正确的单元测试?
问题根源
你遇到的问题本质有两点:
- 默认情况下Rx.NET的大量操作符会使用异步调度器(比如
DefaultScheduler/ThreadPoolScheduler),测试方法主线程执行完毕退出时,Subscribe里的回调还没被调度执行 - 你当前的测试没有等待
result这个Observable完成就直接结束,自然不会触发断言逻辑
修复方案
分为两种实现方式,可根据场景选择:
1. 快速临时修复
适合逻辑简单、不需要模拟时间/异步时序的场景,直接阻塞等待Observable执行完成后再做断言:
[TestMethod] public void TestMethod1() { string userName = "Moeen"; string password = "moeen123"; string instrument = "Bass"; _loggerMock = new Mock<ILogger>(); _loggerMock.Setup(x => x.RequestInput(Messages.EnterUsername)).Returns(Observable.Return(userName)); _loggerMock.Setup(x => x.RequestInput(Messages.EnterPassword)).Returns(Observable.Return(password)); var options = Enum.GetValues(typeof(Instrument)).Cast<Instrument>().Select(val => val.ToString()); _loggerMock.Setup(x => x.RequestInput($"{Messages.SelectInstrument} [{string.Join("|", options)}]")).Returns(Observable.Return(instrument)); _loggerMock.Setup(x => x.ShowMessage("")); _backendMock = new UnreliableFakeBackend(); var result = new SignupFlow(_backendMock, _loggerMock.Object).Run(); // 阻塞等待Observable返回第一个结果,所有回调会执行完成后再往下走 var actualOutput = result.FirstAsync().Wait(); // 这里的断言会正常触发 Assert.AreEqual(true,false); }
如果你的Observable可能抛出异常,在外层加try-catch捕获后做异常断言即可。
2. 官方推荐规范测试方案(使用测试调度器)
适合需要模拟延迟、超时、时序等复杂异步场景的测试,完全可控无异步不稳定问题:
第一步:安装依赖
安装NuGet包Microsoft.Reactive.Testing
第二步:改造业务类支持调度器注入
不要在SignupFlow里硬编码调度器,改为支持构造函数注入,方便测试时传入测试调度器:
public class SignupFlow { private readonly IBackend _backend; private readonly ILogger _logger; private readonly IScheduler _taskScheduler; private readonly IScheduler _timeoutScheduler; // 生产环境默认构造函数,使用系统默认调度器 public SignupFlow(IBackend backend, ILogger logger) : this(backend, logger, TaskPoolScheduler.Default, DefaultScheduler.Instance) { } // 测试专用构造函数,注入测试调度器 public SignupFlow(IBackend backend, ILogger logger, IScheduler taskScheduler, IScheduler timeoutScheduler) { _backend = backend; _logger = logger; _taskScheduler = taskScheduler; _timeoutScheduler = timeoutScheduler; } public IObservable<SignupResult> Run() { // 所有需要调度器的操作都使用注入的实例,比如Delay、ObserveOn、Timer等 return _logger.RequestInput(Messages.EnterUsername) .Delay(TimeSpan.FromSeconds(3), _timeoutScheduler) .SelectMany(username => _logger.RequestInput(Messages.EnterPassword)) .ObserveOn(_taskScheduler); // 剩余业务逻辑省略 } }
第三步:改造测试代码使用TestScheduler
[TestMethod] public void TestMethod1() { // 初始化测试调度器,所有时序由它完全控制 var testScheduler = new TestScheduler(); string userName = "Moeen"; string password = "moeen123"; string instrument = "Bass"; _loggerMock = new Mock<ILogger>(); // 所有Observable创建都传入测试调度器 _loggerMock.Setup(x => x.RequestInput(Messages.EnterUsername)).Returns(Observable.Return(userName, testScheduler)); _loggerMock.Setup(x => x.RequestInput(Messages.EnterPassword)).Returns(Observable.Return(password, testScheduler)); var options = Enum.GetValues(typeof(Instrument)).Cast<Instrument>().Select(val => val.ToString()); _loggerMock.Setup(x => x.RequestInput($"{Messages.SelectInstrument} [{string.Join("|", options)}]")).Returns(Observable.Return(instrument, testScheduler)); _loggerMock.Setup(x => x.ShowMessage("")); _backendMock = new UnreliableFakeBackend(); // 注入测试调度器 var result = new SignupFlow(_backendMock, _loggerMock.Object, testScheduler, testScheduler).Run(); object actualResult = null; result.Subscribe(x => { actualResult = x; Assert.AreEqual(true,false); }); // 推进调度器执行所有已调度任务,相当于瞬间走完所有异步流程 testScheduler.Start(); // 也可以在这里做断言,此时所有回调已经执行完成 Assert.IsNotNull(actualResult); }
改造完成后Subscribe里的断言会被正常触发,测试会正确失败,替换为你实际的业务断言即可。
内容的提问来源于stack exchange,提问作者virtouso
相关产品推荐
相关产品推荐

