【问题标题】:Using DI container in unit tests在单元测试中使用 DI 容器
【发布时间】:2015-12-12 05:25:03
【问题描述】:

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

不过,我们还没有使用 Simple Injector 来管理单元测试的依赖关系树。相反,我们一直在手动更新所有内容。

我刚刚花了几天时间进行一次重大的重构,几乎所有的时间都花在了在我们的单元测试中修复这些手动构建的依赖树上。

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

谁有管理这些的方法?

【问题讨论】:

  • 不确定您要寻找什么模式。为什么不让你的容器在测试初始化​​(例如 xunit 的构造函数)?模式很简单——组合。
  • 如果你真的对单元测试模式感兴趣,你应该阅读xUnit Test Patterns

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


【解决方案1】:

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

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

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

如果您正在编写端到端或“场景”测试,您需要在其中测试整个系统,那么设置您的 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)
}

【讨论】:

  • 我相信Object Mother是单元测试中构建器模式的名称。顺便说一句,很好的答案。 +1
  • @sotn:这似乎不是同一个问题,我没有看到任何与此相矛盾的答案。您能否直接链接到您正在谈论的答案,并解释它与此“相反”的原因?
  • 顺便说一下,AutoFixture 及其扩展非常棒。我强烈推荐它。我将它与最小起订量一起使用,它允许您返回模拟对象及其所有依赖项。
  • @YamaçKurtuluş:感谢分享。自从我发布这个答案以来的几年里,我有机会使用 AutoFixture,我同意。我将它与Automoq extension 结合起来,并在我的设置方法中冻结模拟对象。通过添加一些自定义,我避免了我的模型需要 Builder/Factory 对象。我不必用无意义的字符串和数字弄乱我的代码。太棒了。
【解决方案2】:

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

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

在单元测试中使用 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 Doing_something_will_always_log_a_message()
{
    // Arrange
    var logger = new ListLogger();

    ClassUnderTest sut = CreateClassUnderTest(logger: logger);

    // Act
    sut.DoSomething();
    
    // Arrange
    Assert.IsTrue(logger.Count > 0);    
}

如果您有兴趣学习如何编写可读、可信赖和可维护 (RTM) 测试,Roy Osherove 的书The Art of Unit Testing (second edition) 是一本极好的读物。这极大地帮助了我理解编写出色的单元测试。如果您有兴趣深入了解依赖注入及其相关模式,请考虑阅读Dependency Injection Principles, Practices, and Patterns(我与人合着)。

【讨论】:

    猜你喜欢
    • 2017-01-13
    • 2015-12-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-27
    • 2015-11-24
    相关资源
    最近更新 更多