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

时间:2018-03-14 18:48:13

标签: c# xamarin reactiveui

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

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

1 个答案:

答案 0 :(得分:2)

免责声明:我不知道SPLAT是什么。我写的所有内容都是关于C#,DI,多线程,测试和常见陷阱的一般知识。 SPLAT以一种明智的方式解决它的可能性很小。非零机会。言归正传。

我猜你在服务定位器(反)模式中使用DI。我猜你的Locator.CurrentMutable.xxx是一个全局静态的便利设备,你可以随处访问并随意询问它。因此,它被搞砸了多线程*)和/或测试。周期。

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

  

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

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

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

这是正确的行为。

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

如果您坚持使用服务定位器模式,至少要使其为非静态模式。每个测试必须有自己的分辨率上下文。如果共享它们,测试将相交并失败。

最简单的解决方案是丢弃服务定位器模式。显然,这是不可能的,因为你告诉使用你有一个很大的代码库。

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

如果你的测试是纯粹的和内部单线程的,你可以通过Locator.CurrentMutableLocator 线程静态或类似的东西来实现这一点,比如'异步上下文'或您认为更好的任何内容。但是你应该只在测试中做到这一点,因为在实际应用程序中使线程静态可能会破坏它,因为你的应用程序并没有考虑到这一点。

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

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

...但这确实会阻止您在单独的进程中运行它们。

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

  • 参加测试
  • 创建5-10-20-100个工人进程(可配置?)
  • 分发测试以运行工作进程
  • 每个工作进程都会参加测试
  • 每个工作进程逐个运行他的测试,而不是并行

不是预先分发,而是可以对它们进行汇集和出列,以确保不会出现一个进程因为一次测试需要更长时间等而导致其中的99个测试仍然存在的情况等等。由您决定。最重要的是,如果你的服务定位器是全局的,并且你在每个测试的基础上注册了mocks,那么不要在单个进程中并行化它们。

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