不久前,我阅读了Martin Fowler 的Mocks Aren't Stubs文章,我必须承认我有点害怕外部依赖会增加复杂性,所以我想问一下:
单元测试时使用的最佳方法是什么?
总是使用模拟框架来自动模拟被测试方法的依赖关系会更好,还是更喜欢使用更简单的机制,例如实例测试存根?
不久前,我阅读了Martin Fowler 的Mocks Aren't Stubs文章,我必须承认我有点害怕外部依赖会增加复杂性,所以我想问一下:
单元测试时使用的最佳方法是什么?
总是使用模拟框架来自动模拟被测试方法的依赖关系会更好,还是更喜欢使用更简单的机制,例如实例测试存根?
正如口头禅所说的那样,“去做最简单的事情,可能会奏效。”
始终避免使用模拟,因为它们会使测试变得脆弱。您的测试现在对实现调用的方法有了复杂的了解,如果模拟接口或您的实现发生变化……您的测试会中断。这很糟糕,因为您将花费额外的时间让您的测试运行,而不仅仅是让您的 SUT 运行。测试不应不恰当地与实现密切相关。
所以用你最好的判断..我更喜欢模拟,因为它可以帮助我用 n>>3 方法更新一个假类。
更新结语/
深思熟虑:(感谢 Toran Billups 以模拟测试为例。见下文)
嗨,道格,我认为我们已经超越了另一场圣战 - 经典 TDDers 与 Mockist TDDers。我想我属于前者。
由于期望,我通常更喜欢使用模拟。当您在返回值的存根上调用方法时,它通常只会返回一个值。但是,当您在 mock 上调用方法时,它不仅返回一个值,而且还强制您设置该方法甚至首先被调用的期望。换句话说,如果你设置了一个期望然后不调用那个方法,就会抛出一个异常。当你设定一个期望时,你实际上是在说“如果这个方法没有被调用,那就是出了问题”。反之亦然,如果你在 mock 上调用一个方法并且没有设置期望,它会抛出一个异常,本质上是在说“嘿,你在做什么你没想到的时候调用了这个方法”。
有时您不希望对您调用的每个方法都有期望,因此一些模拟框架将允许类似于模拟/存根混合体的“部分”模拟,因为只有您设置的期望被强制执行,并且所有其他方法调用都被处理更像是一个存根,因为它只返回一个值。
不过,我能想到的一个使用存根的有效地方是,当您将测试引入遗留代码时。有时,通过子类化您正在测试的类来制作存根比重构所有内容以使模拟变得容易甚至成为可能更容易。
而对此...
始终避免使用模拟,因为它们会使测试变得脆弱。您的测试现在对实现调用的方法有了复杂的了解,如果模拟接口发生变化……您的测试会中断。所以用你最好的判断..<
...我说如果我的界面发生变化,我的测试最好中断。因为单元测试的全部意义在于它们准确地测试了我现在存在的代码。
这仅取决于您正在进行的测试类型。如果您正在进行基于行为的测试,您可能需要一个动态模拟,以便您可以验证是否发生了与您的依赖关系的某些交互。但是,如果您正在进行基于状态的测试,您可能需要一个存根,以便验证值/等
例如,在下面的测试中,您注意到我将视图存根,以便我可以验证属性值是否已设置(基于状态的测试)。然后,我创建服务类的动态模拟,以便确保在测试期间调用特定方法(基于交互/行为的测试)。
<TestMethod()> _
Public Sub Should_Populate_Products_List_OnViewLoad_When_PostBack_Is_False()
mMockery = New MockRepository()
mView = DirectCast(mMockery.Stub(Of IProductView)(), IProductView)
mProductService = DirectCast(mMockery.DynamicMock(Of IProductService)(), IProductService)
mPresenter = New ProductPresenter(mView, mProductService)
Dim ProductList As New List(Of Product)()
ProductList.Add(New Product())
Using mMockery.Record()
SetupResult.For(mView.PageIsPostBack).Return(False)
Expect.Call(mProductService.GetProducts()).Return(ProductList).Repeat.Once()
End Using
Using mMockery.Playback()
mPresenter.OnViewLoad()
End Using
'Verify that we hit the service dependency during the method when postback is false
Assert.AreEqual(1, mView.Products.Count)
mMockery.VerifyAll()
End Sub
最好使用组合,您必须使用自己的判断。这是我使用的指南:
第二种嘲笑是一种必要的邪恶。这里真正发生的事情是,无论您使用存根还是模拟,在某些情况下,您必须与您的代码耦合得比您想要的更多。发生这种情况时,最好使用模拟而不是存根,因为您会知道这种耦合何时中断,并且您的代码不再按照您的测试认为的方式编写。当你这样做时,最好在你的测试中留下评论,这样无论谁破坏它都知道他们的代码没有错,测试就是。
再说一次,这是一种代码味道,也是最后的手段。如果您发现需要经常这样做,请尝试重新考虑编写测试的方式。
别介意统计学家与互动。想想角色和关系。如果一个对象与邻居合作完成它的工作,那么这种关系(在接口中表示)是使用模拟测试的候选对象。如果一个对象是一个有一点行为的简单值对象,那么直接测试它。我看不出手动编写模拟(甚至存根)的意义。这就是我们所有人开始和重构的方式。
如需更长时间的讨论,请考虑查看http://www.mockobjects.com/book
在这篇博文中阅读 Luke Kanies 对这个问题的讨论。他引用了 Jay Fields 的一篇文章,该文章甚至建议使用 [a 等价于 ruby's/mocha's] stub_everything 以使测试更加健壮。引用 Fields 的最后一句话:“Mocha 让定义模拟就像定义存根一样容易,但这并不意味着你应该总是更喜欢模拟。事实上,我通常更喜欢存根并在必要时使用模拟。 "