当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使用两个调度程序 - 一个控制延迟,另一个控制观察订阅的线程。此测试实际上失败,因为调度程序以错误的顺序前进。第二个延迟(100毫秒)有多大并不重要 - scheduler1
在scheduler2
之前提前的事实意味着订阅代码(由scheduler1
控制)将不会执行。我们需要另一个电话来提前scheduler1
(或启动它)。
显然在上面的测试代码中,我可以将调用交换到AdvanceBy
并且它可以工作。然而,实际上我是通过它注入我的服务和控制时间。它需要为调度程序选择一个特定的顺序,而且没有真正的方法可以知道“正确”顺序是什么 - 这取决于SUT。
人们使用什么技术来解决此问题?我能想到这些:
TestScheduler
中使用单个TestSchedulerService
,并从所有调度程序属性中返回它
TestSchedulerService
接受一个构造函数参数,告诉它是创建多个TestScheduler
个实例,还是只创建一个。默认只使用一个,因为根据我的经验,需要多个的测试更少见
TestSchedulerService
有点复杂我倾向于那里的最终选项(并且已经突出了代码)。但我想知道是否有更清晰/更清洁的方法来处理这个问题?
答案 0 :(得分:4)
正如您所暗示的那样,这有点“依赖于”情况。我认为你的分析很好。调度程序DI的ISchedulerService
方法是合理的,我在多个项目中成功使用它。
根据我的个人经验,在测试中需要多个不同的TestScheduler是非常罕见的 - 我已经写了很多涉及Rx的单元测试,并且需要在大约5次左右的时间内执行此操作。
因此,我默认使用单个TestScheduler,并且没有用于管理多个TestScheduler方案的特定基础结构。
以其为特色的测试通常会突出显示非常具体的边缘情况,并且只需仔细编写并进行大量评论。
我怀疑没有编写这些测试的用户会理解操作调度程序的细节不会隐藏在框架之后,因为在这些情况下你需要尽可能清楚地看到正在发生的事情。
由于这个原因,我认为我会坚持在我需要的测试代码中直接操作多个调度程序。