在单元测试中,是否有必要模拟所有依赖关系?

3

我正在尝试为一个具有以下构造函数定义的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,那么它应该使模拟变得更加容易。 - Mikko Viitala
我正在使用Moq来模拟这些依赖项(或者至少这是我的意图) - David Jiménez Martínez
什么让嘲弄四个依赖关系变得如此荒谬有趣?大概你只需要在每个被嘲弄的对象上设置一两个方法就可以了吧? - Chris
但是如果我这样做,测试中不会泄露实现细节吗? - David Jiménez Martínez
2个回答

6

你需要为所有依赖提供测试替身,不一定是模拟对象。

幸运的是,现在已经是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集成。

声明:我是该项目的贡献者之一。


很有趣,那我需要的时候可以自定义这些自动生成的模拟吗?这些模拟的方法调用/属性/任何内容会返回什么? - David Jiménez Martínez
1
@DavidJiménezMartínez 是的,你可以使用 Freeze。我已经在我的帖子中添加了一个示例,说明如何进行额外的设置。这个自定义将设置所有模拟对象成员,例如:mockObject.Setup(x => x.Id).Returns(fixture.Create<int>())。基本上,fixture会递归调用自己,直到创建完整个依赖图。 - dcastro
1
请查看备忘单(尽管有点过时,但仍然有用)。此外,如果您不想让AutoFixture自动为您设置模拟对象,您可以使用AutoMoqCustomization自定义选项,它只是注入了全新的模拟对象。 - dcastro
1
我有一个理论上的问题。当你在TDD中测试和定义模拟行为时,你是不是部分地泄露了你想要测试的方法或者它应该如何行动的内部情况,而不是检查函数的输入/输出?只是问一下。 - David Jiménez Martínez
1
@DavidJiménezMartínez 这是一个非常棒的问题,我自己也问过很多次。TDD 的核心在于专注于代码单元的外部行为和使用,但是必须注入和配置测试替身却又产生了一个巨大的悖论。如果你找到了一个令人满意的答案,请告诉我 :) - dcastro
1
@DavidJiménezMartínez 一些类似的问题,值得一读:[1](http://programmers.stackexchange.com/questions/198453/is-there-a-point-to-unit-tests-that-stub-and-mock-everything-public),[2](http://programmers.stackexchange.com/questions/234024/unit-testing-behaviours-without-coupling-to-implementation-details),[3](http://googletesting.blogspot.co.uk/2013/05/testing-on-toilet-dont-overuse-mocks.html) - dcastro

2
不,你不需要。您可以使用的测试对象实现的不同概念称为“测试替身(Test Doubles)”。Mock是Gerard Meszaros在他的书中定义的Test Double类型之一:
  • 虚拟(Dummy)对象被传递但实际上从未使用。通常它们只用于填充参数列表。
  • 伪造(Fake)对象实际上具有工作实现,但通常采取某些捷径,使它们不适合生产(InMemoryTestDatabase就是一个很好的例子)。
  • 存根(Stub)为调用期间进行的调用提供了预先制定的答案,通常对除测试程序之外的任何内容都不做出响应。
  • 间谍(Spy)是同时记录一些信息的存根,这些信息基于它们被调用的方式。其中一种形式可能是记录发送的邮件数量的电子邮件服务。
  • 模拟(Mock)预先编程了期望,这些期望形成了它们所期望接收的调用的规范。如果它们收到了意料之外的调用,它们可以抛出异常,并在验证期间进行检查以确保它们得到了所有期望的调用。

您只需要提供必要数量的存根、虚拟对象和哑对象,以使测试通过。

哑对象生成非常简单,可能足以满足您的需求。例如,一个构造函数接收IEmailer和ILogWriter。如果您只测试Log方法,则只需要提供足够的IEmailer实现即可避免测试引发参数异常。

此外,关于子依赖项的问题…… Moq会为您解决这个问题,因为Moq接口的实现不会带有依赖项。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接