C# 测试使用多个调度程序的代码的技术
当SUT依赖于多个调度器时,保持测试代码简洁和集中的最佳方法是什么?也就是说,避免虚假调用以推进多个不同的调度程序 到目前为止,我的技术一直是定义一个提供调度器的应用程序级服务:C# 测试使用多个调度程序的代码的技术,c#,.net,unit-testing,system.reactive,C#,.net,Unit Testing,System.reactive,当SUT依赖于多个调度器时,保持测试代码简洁和集中的最佳方法是什么?也就是说,避免虚假调用以推进多个不同的调度程序 到目前为止,我的技术一直是定义一个提供调度器的应用程序级服务: public interface ISchedulerService { IScheduler DefaultScheduler { get; } IScheduler SynchronizationContextScheduler { get; } IScheduler TaskPoolS
public interface ISchedulerService
{
IScheduler DefaultScheduler { get; }
IScheduler SynchronizationContextScheduler { get; }
IScheduler TaskPoolScheduler { get; }
// other schedulers
}
然后,应用程序组件注入了一个isSchedulerService
实例,对于任何需要调度器的反应管道,它都是从服务中获得的。然后,测试代码可以使用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
}
}
但是,我发现当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
我倾向于最后一个选项(并且已经添加了代码)。但我想知道是否有更清晰的方法来处理这个问题?正如你所暗示的,这有点“视情况而定”。我认为你的分析很好。用于调度程序DI的
isSchedulerService
方法是可靠的,我已经在多个项目中成功地使用了它
根据我个人的经验,在一个测试中需要多个不同的测试调度器是非常罕见的——我已经编写了上千个涉及Rx的单元测试,大概有五次左右需要这样做
因此,我默认使用单个TestScheduler,并且没有用于管理多个TestScheduler场景的特定基础设施
具有这些特性的测试通常会突出显示非常特定的边缘情况,并且只需仔细编写和大量注释即可
我怀疑没有编写这些测试的用户会喜欢操纵调度程序的细节,而不是隐藏在框架后面,因为在这些情况下,您需要尽可能清楚地看到正在发生的事情
出于这个原因,我认为我会坚持在需要的测试代码中直接操作多个调度程序
[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);
}