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

Ken*_*art 3 .net c# unit-testing system.reactive

当一个SUT依赖多个调度程序时,使测试代码简洁而集中的最佳方法是什么?即,避免虚假调用来推进多个不同的调度程序。

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

public interface ISchedulerService
{
    IScheduler DefaultScheduler { get; }

    IScheduler SynchronizationContextScheduler { get; }

    IScheduler TaskPoolScheduler { get; }

    // other schedulers
}
Run Code Online (Sandbox Code Playgroud)

然后,应用程序组件具有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
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,测试代码可以控制时间:

var scheduler = new TestSchedulerService();
var sut = new SomeClass(scheduler);

scheduler.AdvanceBy(...);
Run Code Online (Sandbox Code Playgroud)

但是,我发现当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);
}
Run Code Online (Sandbox Code Playgroud)

在这里,SUT使用两个调度程序-一个控制延迟,另一个控制观察预订的线程。由于调度程序以错误的顺序前进,因此该测试实际上失败了。第二个延迟(100毫秒)有多大无关紧要- scheduler1提前的事实scheduler2意味着scheduler1将不执行预订代码(由控制)。我们将需要另一个调用来推进scheduler1(或启动它)。

显然,在上面的测试代码中,我可以将调用交换到AdvanceBy周围,​​并且可以正常工作。但是,实际上,我是通过服务注入时间并控制时间。它需要为调度程序选择特定的顺序,并且没有真正的方法知道“正确”的顺序是什么-这取决于SUT。

人们使用什么技术来解决此问题?我可以想到这些:

  • 从调度程序服务中删除时间控制方法,而是要求调用者推进特定的调度程序
    • 优点:强制测试代码按他们选择的顺序推进调度程序
    • 缺点:膨胀测试代码并掩盖意图
  • 仅在中使用一个,然后从所有调度程序属性TestScheduler中将其TestSchedulerService返回
    • 优点:它解决了这个特定问题
    • 缺点:对于那些需要它的测试没有细粒度的控制
  • TestSchedulerService采取构造函数的参数告诉它是否要创建多个TestScheduler实例,或只有一个。默认情况下只使用一个,因为在我的经验中,需要多个测试很少
    • 优点:解决问题而无需放弃对那些需要它的测试的控制
    • 缺点:有点神奇,这使它TestSchedulerService有点复杂

我倾向于那里的最后一个选项(并增加了代码)。但是我想知道是否有更清晰/更清洁的方式来处理此问题?

Jam*_*rld 5

正如您所暗示的,这有点“取决于”情况。我认为您的分析很好。ISchedulerService调度程序DI 的方法是正确的,我已经在多个项目中成功使用了它。

以我的个人经验,在一个测试中需要多个不同的TestScheduler极为罕见-我已经编写了数千个涉及Rx的单元测试,并且可能需要在大约五次左右进行。

因此,我默认使用单个TestScheduler,并且没有用于管理多个TestScheduler方案的特定基础结构。

具有这些功能的测试通常会突出显示非常具体的边缘情况,并且只需精心编写并进行大量评论即可。

我怀疑没有编写这些测试的用户会喜欢不隐藏在框架后面的调度程序的细节,因为在这种情况下,您需要尽可能清楚地了解正在发生的事情。

仅出于这个原因,我认为我会坚持在需要的测试代码中直接操纵多个调度程序。