在单元测试中使用DI容器

时间:2015-09-15 20:13:29

标签: c# unit-testing dependency-injection ioc-container simple-injector

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

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

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

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

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

2 个答案:

答案 0 :(得分:14)

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

  • 如果你发现你有很多重复的代码用于new你所创建的所有模拟的类实例,一个有用的策略是创建你的所有模拟并创建在Setup方法中测试对象的实例(这些都可以是私有实例字段),然后每个单独的测试""""区域只需要在模拟所需的方法上调用适当的Setup()代码。这样,每个测试类最终只有一个new PersonController(...)语句。
  • 如果您需要创建大量域/数据对象,那么创建以合理的值进行测试的Builder对象非常有用。因此,不是在整个代码中调用一个巨大的构造函数,而是使用一堆伪值,而你大多只是调用,例如,var person = new PersonBuilder().Build(),可能只需要几个链接方法调用你的数据特别关心那个测试。您可能也对AutoFixture感兴趣,但我从未使用它,所以我无法保证。

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

如果您正在编写端到端或“#34”场景"测试,您需要测试整个系统,然后设置您的DI框架,利用您的真实产品使用的相同配置代码是有意义的。您可以稍微更改配置,以便更好地控制用户登录等事项。您仍然可以利用您为构建数据而创建的其他构建器类。

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

答案 1 :(得分:10)

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

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

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

过去曾帮助过我的一个模式是使用一个简单的测试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的书“单元测试的艺术”(第二版)。这对我编写出色的单元测试有很大的帮助。如果您有兴趣了解有关依赖注入的更多信息,my book是最明显的书。