【发布时间】:2015-02-13 20:06:31
【问题描述】:
当一个 SUT 依赖于多个调度程序时,保持测试代码简洁和集中的最佳方法是什么?也就是说,避免虚假调用来推进多个不同的调度程序。
到目前为止,我的技术一直是定义一个提供调度程序的应用程序级服务:
public interface ISchedulerService
{
IScheduler DefaultScheduler { get; }
IScheduler SynchronizationContextScheduler { get; }
IScheduler TaskPoolScheduler { get; }
// other schedulers
}
然后应用程序组件会注入一个ISchedulerService 的实例,对于任何需要调度程序的反应式管道,它都是从服务中获取的。然后测试代码可以使用TestSchedulerService的实例:
public sealed class TestSchedulerService : ISchedulerService
{
private readonly TestScheduler defaultScheduler;
private readonly TestScheduler synchronizationContextScheduler;
private readonly TestScheduler taskPoolScheduler;
// other schedulers
public TestSchedulerService()
{
this.defaultScheduler = new TestScheduler();
this.synchronizationContextScheduler = new TestScheduler();
this.taskPoolScheduler = new TestScheduler();
}
public IScheduler DefaultScheduler
{
get { return this.defaultScheduler; }
}
public IScheduler SynchronizationContextScheduler
{
get { return this.synchronizationContextScheduler; }
}
public IScheduler TaskPoolScheduler
{
get { return this.taskPoolScheduler; }
}
public void Start()
{
foreach (var testScheduler in this.GetTestSchedulers())
{
testScheduler.Start();
}
}
public void AdvanceBy(long time)
{
foreach (var testScheduler in this.GetTestSchedulers())
{
testScheduler.AdvanceBy(time);
}
}
public void AdvanceTo(long time)
{
foreach (var testScheduler in this.GetTestSchedulers())
{
testScheduler.AdvanceTo(time);
}
}
private IEnumerable<TestScheduler> GetTestSchedulers()
{
yield return this.defaultScheduler;
yield return this.synchronizationContextScheduler;
yield return this.taskPoolScheduler;
// other schedulers
}
}
测试代码可以这样控制时间:
var scheduler = new TestSchedulerService();
var sut = new SomeClass(scheduler);
scheduler.AdvanceBy(...);
但是,我发现当 SUT 使用多个调度程序时,这可能会导致问题。考虑这个简单的例子:
[Fact]
public void repro()
{
var scheduler1 = new TestScheduler();
var scheduler2 = new TestScheduler();
var pipeline = Observable
.Return("first")
.Concat(
Observable
.Return("second")
.Delay(TimeSpan.FromSeconds(1), scheduler2))
.ObserveOn(scheduler1);
string currentValue = null;
pipeline.Subscribe(x => currentValue = x);
scheduler1.AdvanceBy(TimeSpan.FromMilliseconds(900).Ticks);
scheduler2.AdvanceBy(TimeSpan.FromMilliseconds(900).Ticks);
Assert.Equal("first", currentValue);
scheduler1.AdvanceBy(TimeSpan.FromMilliseconds(100).Ticks);
scheduler2.AdvanceBy(TimeSpan.FromMilliseconds(100).Ticks);
Assert.Equal("second", currentValue);
}
这里 SUT 使用两个调度程序 - 一个控制延迟,另一个控制观察订阅的线程。该测试实际上失败了,因为调度程序以错误的顺序前进。第二个延迟(100ms)有多大并不重要——scheduler1 在scheduler2 之前提前意味着订阅代码(由scheduler1 控制)将不会被执行。我们需要另一个电话来推进scheduler1(或启动它)。
显然,在上面的测试代码中,我只需将调用交换到AdvanceBy 就可以了。然而,实际上我是在注入我的服务并通过它控制时间。它需要为调度程序选择一个特定的顺序,并且没有真正的方法可以知道“正确”的顺序是什么——这取决于 SUT。
人们使用什么技术来解决这个问题?我能想到这些:
- 从调度程序服务中删除时间控制方法,而是要求调用者推进特定的调度程序
- 优点:强制测试代码按照他们选择的顺序推进调度程序
- 缺点:使测试代码膨胀并掩盖意图
- 仅在
TestSchedulerService中使用单个TestScheduler并从所有调度程序属性中返回它- 优点:它解决了这个特定问题
- 缺点:对于需要它的测试没有细粒度控制
- 让
TestSchedulerService接受一个构造函数参数,告诉它是创建多个TestScheduler实例,还是只创建一个。默认只使用一个,因为根据我的经验,需要多个的测试很少见- 优点:在不放弃对需要它的测试的控制的情况下解决问题
- 缺点:有点神奇,它使
TestSchedulerService有点复杂
我倾向于那里的最后一个选项(并且已经删除了代码)。但我想知道是否有更清晰/更清洁的方法来处理这个问题?
【问题讨论】:
标签: c# .net unit-testing system.reactive