【问题标题】:Techniques for testing code that uses multiple schedulers测试使用多个调度程序的代码的技术
【发布时间】: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)有多大并不重要——scheduler1scheduler2 之前提前意味着订阅代码(由scheduler1 控制)将不会被执行。我们需要另一个电话来推进scheduler1(或启动它)。

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

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

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

我倾向于那里的最后一个选项(并且已经删除了代码)。但我想知道是否有更清晰/更清洁的方法来处理这个问题?

【问题讨论】:

    标签: c# .net unit-testing system.reactive


    【解决方案1】:

    正如您所暗示的,这有点“视情况而定”。我觉得你的分析很好。调度器DI的ISchedulerService方法很不错,我已经在多个项目中成功使用过。

    根据我的个人经验,在一个测试中需要多个不同的 TestScheduler 是非常罕见的 - 我已经编写了超过一千个涉及 Rx 的单元测试,并且可能需要在五次左右的情况下这样做。

    所以我默认使用单个 TestScheduler,并且没有用于管理多个 TestScheduler 场景的特定基础架构。

    具有它们的测试通常会突出非常具体的边缘情况,并且只是经过精心编写和大量注释。

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-12-06
      • 2014-04-29
      • 2017-12-26
      • 2014-01-30
      • 2014-08-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多