【问题标题】:Run unit tests in parallel for code using Splat使用 Splat 为代码并行运行单元测试
【发布时间】:2018-03-14 18:48:12
【问题描述】:

我们使用 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);
    }
}

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

【问题讨论】:

标签: c# xamarin reactiveui


【解决方案1】:

免责声明:我不知道 SPLAT 是什么。我写的都是关于 C#、DI、多线程、测试和常见陷阱的通用知识。 SPLAT 以某种出色的方式解决它的可能性很小。非零机会。接近于零。


我猜你在服务定位器(反)模式中使用 DI。我猜你的Locator.CurrentMutable.xxx 是一个全局静态的方便的东西,你可以在任何地方访问它并要求它做任何事情。所以,它被多线程*)和/或测试搞砸了。期间。

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

Locator.Current 是一个静态变量,可以在启动时设置,以 使 Splat 适应其他 DI/IoC 框架。这通常是个坏主意。

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

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

这是正确的行为。

错误出现在您的测试设置、测试运行或 DI 架构中。

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

最简单的解决方案是删除服务定位器模式。显然这是不可能的,因为你告诉我们你有一个庞大的代码库。

另一种选择是对定位器进行去静电处理。尝试使它可以“上下文相关”,以便每个测试都有自己的服务提供者/定位器的单独实例。这将解决问题,因为注册将发生在不同的实例上,并且不会相交和覆盖。

如果您的测试是普通的并且内部是单线程的,您可以通过使 Locator.CurrentMutableLocator thread-static 或类似的东西来实现这一点,例如“异步上下文”或任何你认为更好的东西。但是您应该只在测试中这样做,因为在实际应用程序中将其设为线程静态可能会破坏它,因为您的应用程序在设计时并未考虑到这一点。

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

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

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

获取文档,查看源代码,然后为自己编写一个全新的 xUnit 运行器,这将:

  • 运行测试
  • 创建 5-10-20-100 个工作进程(可配置?)
  • 分发测试以在工作进程上运行
  • 每个工作进程都参与了测试
  • 每个工作进程一个接一个地运行他的测试,而不是并行

您可以将它们池化和出队,而不是预先分配,以确保不会出现一个进程在剩余 99 个测试中徘徊的情况,因为一个测试需要更长的时间等等。由您决定。最重要的是,如果您的服务定位器是全局的并且您在每个测试的基础上在其中注册模拟,请不要在单个进程中并行化它们。

*) 是的,我知道互斥体/等。但是除非从消费者的角度来看它是不可变的,否则它仍然被搞砸了,而这里显然它不是不可变的。

【讨论】:

  • 您确实是对的,我们严重依赖服务定位器反模式。由于我们类的结构,我们无法/不愿意像某些类那样注入尽可能多的依赖项。为了解决这个问题,我们切换到工厂支持的提供程序类,它为我们提供了连接的视图模型。然而,我们首先为每个构造函数注入服务定位器。这使我们能够将依赖项与静态变量解耦,从而使我们能够并行执行测试。尽管这隐藏了类的依赖关系,但这是朝着正确方向迈出的一步!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多