运行 使用 Splat 对代码进行并行单元测试

Run unit tests in parallel for code using Splat

我们使用 ReactiveUI 开发移动应用程序,因此使用 Splat 作为依赖注入框架。在 运行 进行单元测试时,我们决定让它们 运行 并行以提高 IDE 中的反馈速度。我们注意到某些测试失败了,因为我们的 SUT 使用了 Splat,因此我们使用 splat 在测试中注入模拟。我确信其他使用 Splat 的团队也发生过这种情况,所以我想知道是否有内置的方法来绕过这个障碍。

public interface IDependency
{
    void Invoke();
}

public class SUT1
{
    private IDependency dependency;
    public SUT1()
    {
        dependency = Locator.CurrentMutable.GetService<IDependency>();      
    }
    public void TestThis()
    {
        dependency.Invoke();
    }
}

public class SUT2
{
   private IDependency dependency;
    public SUT2()
    {
        dependency = Locator.CurrentMutable.GetService<IDependency>();      
    }
    public void TestThisToo()
    {
         dependency.Invoke();
    }
}

public class SUT1_Test()
{
    [Fact]
    public void TestSUT1()
    {
        var dependencyMock = new Mock<IDependency>();
        Locator.CurrentMutable.Register(() => dependencyMock.Object, typeof(IDependency));
        var sut = new SUT1();
        sut.TestThis();
        dependencyMock.Verify(x => x.Invoke(), Times.Once);
    }
}

public class SUT2_Test()
{
    [Fact]
    public void TestSUT2()
    {
        var dependencyMock = new Mock<IDependency>();
        Locator.CurrentMutable.Register(() => dependencyMock.Object, typeof(IDependency));
        var sut = new SUT2();
        sut.TestThisToo();
        dependencyMock.Verify(x => x.Invoke(), Times.Once);
    }
}

如果测试 运行 以在调用函数之前注入新 mock 的方式进行,调用验证将变得一团糟。禁用并行化使我们每次都能获得正确的结果,但代价是用于 运行 按顺序进行测试的时间。

免责声明:我不知道 SPLAT 是什么。我写的所有内容都是关于 C#、DI、多线程、测试和常见陷阱的通用知识。 SPLAT 有可能以某种绝妙的方式解决它。 Non-zero 机会。接近于零。


我猜你在服务定位器(反)模式中使用了 DI。我猜你的 Locator.CurrentMutable.xxx 是一个 globally-static 方便的东西,你可以在任何地方访问它并向它请求任何东西。所以,它搞砸了多线程*) and/or 测试。期间.

引用自https://github.com/reactiveui/splat#service-location

Locator.Current is a static variable that can be set on startup, to adapt Splat to other DI/IoC frameworks. This is usually a bad idea.

所以,嗯,是的,它是静态的。不好。

当您运行并行进行多个测试时,它们都会尝试为同一服务注册自己的模拟,并且它们必然会相交。如果你的 DI 东西是合理的,它会抛出异常。似乎不是,所以最后一次注册可能获胜,所以测试 A 得到了测试 B 注册的模拟。这搞砸了验证,因为测试 A 从模拟 B 读取并且模拟记录的操作来自测试 B 而不是 A,所以从 A 验证失败。

这是正确的行为。

错误出在您的测试设置中,测试 运行ning,或者在您的 DI 架构中。

如果你坚持使用service-locator模式,至少让它成为非静态的。每个测试都必须有自己的解析上下文。如果共享,测试将相交并失败。

最简单的解决方案是放置 service-locator 模式。显然这是不可能的,因为你告诉我们你有一个很大的代码库。

另一种选择是 de-staticize 定位器。尝试使其成为“上下文相关的”,这样每个测试都会有自己的 服务 provider/locator 的单独实例 。这将解决问题,因为注册将发生在不同的实例上,并且不会交叉和覆盖。

如果您的测试是简单的并且在内部 single-threaded,您可以通过 Locator.CurrentMutableLocator thread-static[=66 来实现=] 或类似的东西,例如 'async context' 或任何您认为更好的东西。但是你应该只在 tests 中这样做,因为在实际应用程序中使其成为线程静态可能会破坏它,因为你的应用程序在设计时没有考虑到这一点。

最后,您可以尝试调整测试的方式,而不是玩弄代码、测试代码或服务提供商生命周期 运行。如果 Locator.CurrentMutable 是全局静态的并且必须保持这样,那么...

...那么您的测试不能运行在同一进程中并行(因为它们会相交)...

...但这并不妨碍您 运行 在 单独的进程中

获取文档,查看源代码,并为自己编写一个 b运行d 新的 运行ner for xUnit,这将:

  • 参加考试 运行
  • 创建 5-10-20-100 个工作进程(可配置?)
  • 通过工作进程运行分发测试
  • 每个工作进程都有自己的部分测试
  • 每个工作进程 运行 一个接一个地进行测试,而不是并行进行

而不是 pre-distributing,您可以将它们合并和出队,以确保不会出现这样一种情况:一个进程在他的 99 个测试中徘徊,因为一个测试花费了更长的时间,等等。由您决定。最重要的是,如果您的 service-locator 是全局的并且您在 per-test 基础上在其中注册模拟,则不要将它们并行化 in-single-process。

*) 是的,我知道 mutex/etc。但它仍然被搞砸了,除非从消费者的角度来看它是不可变的,在这里它显然不是不可变的。