在unit testing中使用DI容器

我们一直在使用Simple Injector取得了很好的成功,在一个相当实际的应用程序中。 我们一直在为所有生产类使用构造函数注入,并配置Simple Injector来填充所有内容,而且一切都很好。

但是,我们没有使用Simple Injector来管理unit testing的依赖树。 相反,我们一直在手动创新。

我花了几天时间完成了一次重大的重构,几乎所有的时间都是在我们的unit testing中修复这些手动构造的依赖树。

这让我感到疑惑 – 有没有人有任何模式用于配置他们在unit testing中使用的依赖树? 对我们来说,至少在我们的测试中,我们的依赖树往往相当简单,但有很多。

任何人都有他们用来管理这些的方法吗?

对于真正的unit testing(即那些只测试一个类,并模拟其所有依赖项的测试),使用DI框架没有任何意义。 在这些测试中:

  • 如果你发现你有很多重复的代码用于创建你所创建的所有模拟的类的实例,一个有用的策略是创建所有的模拟并为被测试主题创建实例在您的安装方法中(这些都可以是私有实例字段),然后每个单独的测试的“安排”区域只需要调用它需要模拟的方法的相应Setup()代码。 这样,每个测试类最终只有一个new PersonController(...)语句。
  • 如果您需要创建大量的域/数据对象,那么创建以健全的值开始测试的Builder对象会很有用。 因此,不是在整个代码中调用一个巨大的构造函数,而是使用一堆伪值,而你大多只是调用,例如, var person = new PersonBuilder().Build() ,可能只需要几个链式方法调用您在该测试中特别关注的数据。 您可能也对AutoFixture感兴趣,但我从未使用它,所以我不能保证它。

如果你正在编写集成测试,你需要测试系统的几个部分之间的交互,但你仍然需要能够模拟特定的部分,考虑为你的服务创建Builder类,所以你可以说,例如var personController = new PersonControllerBuilder.WithRealDatabase(connection).WithAuthorization(new AllowAllAuthorizationService()).Build()

如果您正在编写端到端或“场景”测试,您需要测试整个系统,那么设置DI框架是有意义的,利用您的实际产品使用的相同配置代码。 您可以稍微更改配置,以便为您自己提供更好的编程控制,例如用户登录等等。 您仍然可以利用您为构建数据创建的其他构建器类。

 var user = new PersonBuilder().Build(); using(Login.As(user)) { var controller = Container.Get(); var result = controller.GetCurrentUser(); Assert.AreEqual(result.Username, user.Username) } 

在unit testing中不要使用DI容器。 在unit testing中,您尝试单独测试一个类或模块,并且该区域中的DI容器几乎没有用处。

集成测试的情况有所不同,因为您要测试系统中的组件如何集成和协同工作。 在这种情况下,您经常使用生产DI配置并将一些服务替换为虚假服务(例如您的MailService ),但尽可能贴近真实的服务。 在这种情况下,您使用容器来解析整个对象图。

在unit testing中使用DI容器的愿望通常源于无效模式。 例如,如果您尝试在每个测试中创建具有所有依赖项的测试中的类,您将获得大量重复的初始化代码,并且在此类情况下,您的类中的一点变化可能会影响系统并要求您改变了几十个unit testing。 这显然会导致可维护性问题。

在过去帮助我很多的一种模式是使用一个简单的测试SUT特定的工厂方法。 此方法集中创建要测试的类,并最小化在测试类的依赖项发生更改时需要进行的更改量。 这就是这种工厂方法的样子:

 private ClassUnderTest CreateClassUnderTest( ILogger logger = null, IMailSender mailSender = null, IEventPublisher publisher = null) { return new ClassUnderTest( logger ?? new FakeLogger(), mailSender ?? new FakeMailer(), publisher ?? new FakePublisher()); } 

此工厂方法复制包含类的构造函数参数,但使它们都是可选的。 对于任何特定的未提供的依赖项,将注入新的伪实现。

这非常有效,因为在大多数测试中,您只对一两个依赖项感兴趣。 该类可能需要其他依赖项才能运行,但对于该特定测试并不感兴趣。 因此,工厂方法允许您仅提供对手头测试感兴趣的依赖项,同时消除未使用的依赖项的噪声。 因此工厂方法允许您编写以下测试:

 public void Test() { // Arrange var logger = new ListLogger(); ClassUnderTest sut = CreateClassUnderTest(logger: logger); // Act sut.DoSomething(); // Arrange Assert.IsTrue(logger.Count > 0); } 

如果您有兴趣学习如何编写可读,可信和可维护( RTM )测试,我建议您阅读Roy Osherove的书“unit testing的艺术”(第二版)。 这对我帮助很大。