C# 测试使用多个调度程序的代码的技术

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

当SUT依赖于多个调度器时,保持测试代码简洁和集中的最佳方法是什么?也就是说,避免虚假调用以推进多个不同的调度程序

到目前为止,我的技术一直是定义一个提供调度器的应用程序级服务:

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);
}