C# 通过伪造调度对 System.Reactive 可观察对象进行单元测试
一、问题
假设有一个依赖时间的可观察对象,你需要编写不依赖时间的单元测试。依赖时间的可观察对象会使用超时、窗口(或缓冲)以及节流(或采样),现在需要对可观察对象进行单元测试,但单元测试的用时不要过长。
二、解决方案
在单元测试中加入延迟自然是可行的,但是,这样做会造成两个问题:首先,单元测试耗时长;其次,所有单元测试同时运行会导致竞争条件,使计时变得不可预测。
System.Reactive 库的开发人员在设计之初就考虑了测试。其实,System.Reactive 库自身已经过了大量的单元测试。为了能充分地进行单元测试,System.Reactive 引入了调度器这个概念,每个处理时间的 System.Reactive 运算符都是通过这个抽象的调度器实现的。
为了能够测试可观察对象,需要让调用函数指定调度器。比如,可以使用 7.5 节中的 MyTimeoutClass
并添加调度器:
public interface IHttpService
{
IObservable<string> GetString(string url);
}
public class MyTimeoutClass
{
private readonly IHttpService _httpService;
public MyTimeoutClass(IHttpService httpService)
{
_httpService = httpService;
}
public IObservable<string> GetStringWithTimeout(string url,
IScheduler scheduler = null)
{
return _httpService.GetString(url)
.Timeout(TimeSpan.FromSeconds(1), scheduler ?? Scheduler.Default);
}
}
接下来,修改 HTTP 服务存根,这样它能理解调度,然后引入可变延迟:
private class SuccessHttpServiceStub : IHttpService
{
public IScheduler Scheduler { get; set; }
public TimeSpan Delay { get; set; }
public IObservable<string> GetString(string url)
{
return Observable.Return("stub")
.Delay(Delay, Scheduler);
}
}
现在使用 System.Reactive 库中的 TestScheduler
类型,从而有效地掌控(虚拟)时间。
TestScheduler
存在于 System.Reactive 中的一个独立的 NuGet 包里,需要安装Microsoft.Reactive.Testing
NuGet 包才能使用。
凭借 TestScheduler
可以完全掌控时间,而且通常只要设置好参数,然后调用 TestScheduler.Start
就足够了。它会虚拟地推进时间,直到完成所有操作。下面是一个简单的成功单元测试用例:
[TestMethod]
public void MyTimeoutClass_SuccessfulGetShortDelay_ReturnsResult()
{
var scheduler = new TestScheduler();
var stub = new SuccessHttpServiceStub
{
Scheduler = scheduler,
Delay = TimeSpan.FromSeconds(0.5),
};
var my = new MyTimeoutClass(stub);
string result = null;
my.GetStringWithTimeout("http://exampleurl", scheduler)
.Subscribe(r => { result = r; });
scheduler.Start();
Assert.AreEqual("stub", result);
}
代码模拟了半秒的网络延迟。注意,该单元测试耗时不到半秒,这一点很重要。在我的计算机上,它耗时 70 毫秒。半秒的延迟只存在于虚拟时间中。另一个重要的差别是,该单元测试并非异步,由于使用了 TestScheduler
,因此所有的测试可以立即完成。
既然到处都用了测试调度器,那么测试超时情况就简单了:
[TestMethod]
public void MyTimeoutClass_SuccessfulGetLongDelay_ThrowsTimeoutException()
{
var scheduler = new TestScheduler();
var stub = new SuccessHttpServiceStub
{
Scheduler = scheduler,
Delay = TimeSpan.FromSeconds(1.5),
};
var my = new MyTimeoutClass(stub);
Exception result = null;
my.GetStringWithTimeout("http://exampleurl", scheduler)
.Subscribe(_ => Assert.Fail("Received value"), ex => { result = ex; });
scheduler.Start();
Assert.IsInstanceOfType(result, typeof(TimeoutException));
}
上面的单元测试耗时不到 1.5 秒,它通过虚拟时间立即执行了。
三、讨论
本节只略微探讨了 System.Reactive 调度器和虚拟时间。在编写 System.Reactive 代码时,推荐进行单元测试。当代码变得越来越复杂时,就可以放心交由 Microsoft.Reactive.Testing
来处理。
TestScheduler
还有 AdvanceTo
方法和 AdvanceBy
方法,可以在虚拟时间中渐进地前行。在某些情况下能够使用这些方法,但应当尽力让单元测试只专注于测试一个东西。如果需要测试超时,可以写一个简单的单元测试,来局部推进 TestScheduler
,同时确保超时不至于过早发生。然后,推进 TestScheduler
,直到超过超时值,并确保发生了超时。但是,我个人更倾向于尽可能分开进行单元测试。比如,通过一个单元测试确保超时未过早发生,然后通过另一个单元测试确保超时在之后确实发生了。
(完)
相关阅读:
C# 对 async 方法进行单元测试
C# 对预期失败的 async 方法进行单元测试
C# 对 async void 方法进行单元测试
C# 对数据流网格进行单元测试
C# 对 System.Reactive 可观察对象进行单元测试
C# 通过伪造调度对 System.Reactive 可观察对象进行单元测试
评论
发表评论