【问题标题】:Is it necessary to mock all and every dependency in Unit Tests?是否有必要模拟单元测试中的所有依赖项?
【发布时间】:2014-11-24 11:44:52
【问题描述】:

我正在尝试为具有以下构造函数定义的 ASP.NET 创建一个单元测试(在运行实际应用程序时填充 Ninject):

    public OrderController(IViewModelFactory modelFactory, INewsRepository repository, ILoggedUserHelper loggedUserHelper,
        IDelegateHelper delegateHelper, ICustomerContextWrapper customerContext) {
        this.factory = modelFactory;
        this.loggedUserHelper = loggedUserHelper;
        this.delegateHelper = delegateHelper;
        this.customerContext = customerContext;
    }

我想测试 OrderController 类中的方法,但为了隔离它,我必须模拟所有这些依赖项,这变得非常荒谬(可能还必须模拟子依赖项)。

在这种情况下,对此类进行单元测试的最佳实践是什么?

【问题讨论】:

  • 只模拟所有四个注入的依赖项。如果您不使用起订量,它应该会使模拟变得容易得多。
  • 我正在使用 Moq 来模拟这些依赖项(或者至少这是我的意图)
  • 是什么让我们对嘲笑四个依赖项如此可笑?大概你只需要在每个模拟对象上设置一两个方法?
  • 但是如果我这样做,实现细节不会在测试中泄露吗?

标签: c# .net unit-testing mocking


【解决方案1】:

好吧,您必须为所有依赖项提供 测试替身,不一定是模拟。

幸运的是,现在是 21 世纪,有一些工具可以让我们的工作更轻松。您可以使用AutoFixture 创建OrderController 的实例并根据需要注入模拟。

var fixture = new Fixture().Customize(new AutoConfiguredMoqCustomization());
var orderController = fixture.Create<OrderController>();

基本上相当于:

var factory = new Mock<IViewModelFactory>();
var repository = new Mock<INewsRepository>();
var delegateHelper = new Mock<IDelegateHelper >();
var customerContext = new Mock<ICustomerContextWrapper >();

var orderController = new OrderController(factory.Object, repository.Object, delegateHelper.Object, customerContext.Object);

如果这些依赖项依赖于其他类型,那么也会设置这些依赖项。带有 AutoConfiguredMoqCustomization 自定义的 AutoFixture 将构建完整的依赖关系图。

如果您需要访问存储库模拟,以便稍后对其进行一些额外的设置或断言,您可以冻结它。冻结一个类型将使fixture 容器只包含该类型的一个实例,例如:

var fixture = new Fixture().Customize(new AutoConfiguredMoqCustomization());
var repositoryMock = fixture.Freeze<Mock<INewsRepository>>();
repositoryMock.Setup(x => x.Retrieve()).Returns(1);

//the frozen instance will be injected here
var orderController = fixture.Create<OrderController>(); 

repositoryMock.Verify(x => x.Retrieve(), Times.Once);

我在这些示例中使用了 Moq,但 AutoFixture 还与 NSubstitute、RhinoMock 和 Foq 集成。

披露:我是该项目的贡献者之一

【讨论】:

  • @DavidJiménezMartínez 是的,您可以使用Freeze。我在帖子中添加了一个示例,说明如何进行其他设置。此自定义将设置所有 mock 的成员:mockObject.Setup(x =&gt; x.Id).Returns(fixture.Create&lt;int&gt;())。基本上,fixture 会递归调用自己,直到创建整个依赖关系图。
  • 检查cheat sheet(虽然有点过时,但仍然有用)。此外,如果您不希望 autofixture 自动为您设置模拟,您可以使用 AutoMoqCustomization 自定义来代替,它只是注入闪亮的全新模拟。
  • 我有一个最后的理论问题。当您在 TDD 上测试和定义模拟行为时,您不是部分泄漏了您要测试的方法的内部结构,或者它应该如何表现,而不是检查函数的输入/输出吗?只是问问。
  • @DavidJiménezMartínez 这是一个绝妙的问题,我已经问过自己很多次了。 TDD 关注的是代码单元的外部行为和使用,但必须注入和配置测试替身会产生巨大的悖论。如果您找到满意的答案,请告诉我:)
  • @DavidJiménezMartínez 一些类似的问题,值得一读:1, 2, 3
【解决方案2】:

不,你没有。您可以使用的测试对象实现的不同概念称为测试替身。 Mocks 只是 Gerard Meszaros 在his book 中定义的测试替身的一种类型:

  • Dummy 对象被传递但从未实际使用过。通常它们仅用于填充参数列表。
  • Fake 对象实际上有工作实现,但通常采用一些捷径,这使得它们不适合生产(InMemoryTestDatabase 就是一个很好的例子)。
  • 存根为测试期间拨打的电话提供预设答案,通常根本不响应任何超出测试程序的内容。
  • Spies 是存根,它还根据调用方式记录一些信息。其中一种形式可能是电子邮件服务,它记录发送了多少消息。
  • Mocks 预编程了期望,这些期望形成了他们期望接收的调用的规范。如果他们收到意外的呼叫,他们可能会抛出异常,并在验证过程中进行检查以确保他们收到了他们期待的所有呼叫。

您只需要提供尽可能多的存根、伪造品和假人即可通过测试。

假人只需很少的工作即可生成,可能足以涵盖您的场景。一个示例是采用 IEmailer 和 ILogWriter 的构造函数。如果您只测试 Log 方法,则只需提供足够的 IEmailer 实现,以便测试不会引发 Argument 异常。

另外,关于您关于子依赖项的观点...... Moq 会为您处理好这个问题,因为您的接口的 Moq 实现不会接受依赖项。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-07-01
    • 2023-03-13
    • 2016-02-19
    • 1970-01-01
    • 2016-02-19
    • 2013-07-07
    • 1970-01-01
    • 2012-07-11
    相关资源
    最近更新 更多