模拟比存根更好吗?

时间:2008-09-06 19:13:08

标签: unit-testing mocking

前段时间我读了Martin Fowler撰写的Mocks Aren't Stubs文章,我必须承认我有点害怕外部依赖关于增加的复杂性所以我想问:

单元测试时使用的最佳方法是什么?

最好总是使用模拟框架自动模拟被测试方法的依赖关系,还是更喜欢使用更简单的机制,例如测试存根?

6 个答案:

答案 0 :(得分:11)

正如咒语所说'去做最简单的事情,可能有用。'

  1. 如果假班可以完成工作,那就去吧。
  2. 如果您需要一个具有多种方法的接口,请使用模拟框架。
  3. 避免使用模拟总是,因为它们会使测试变得脆弱。您的测试现在对实现调用的方法有复杂的了解,如果模拟的接口或您的实现发生了变化......您的测试会中断。这很糟糕因为你会花费额外的时间让你的测试运行而不是让你的SUT运行。 测试不应与实施不合适。
    因此,请使用您最好的判断..我更喜欢嘲笑它会帮助我保存写作 - 使用n>> 3方法更新假类。

    更新结语/审议:
    (感谢Toran Billups举行的一次模拟测试。见下文)
    嗨,道格,我想我们已经超越了另一场圣战 - 经典TDDers与Mockist TDDers。我想我属于前者。

    • 如果我正在测试#101 Test_ExportProductList,我发现我需要向IProductService.GetProducts()添加一个新的参数。我做到了这个测试绿色。我使用重构工具来更新所有其他引用。现在我发现所有称这个成员的模拟测试现在都爆炸了。然后我必须回去更新所有这些测试 - 浪费时间。为什么ShouldPopulateProductsListOnViewLoadWhenPostBackIsFalse失败?是因为代码坏了吗?相反,测试被打破了。我赞成一个测试失败= 1个地方来修复。嘲弄频率与此相反。存根会更好吗?如果它我有一个fake_class.GetProducts()..确定一个地方改变而不是猎枪手术多个Expect电话。最后这是一个风格的问题..如果你有一个常用的实用方法MockHelper.SetupExpectForGetProducts() - 这也足够..但你会发现这是不常见的。
    • 如果在测试名称上放置白色条带,则测试很难阅读。很多用于模拟框架的管道代码隐藏了正在执行的实际测试。
    • 要求您学习模拟框架的这种特殊风格

答案 1 :(得分:8)

由于期望,我通常更喜欢使用模拟。当您在返回值的存根上调用方法时,它通常只会返回一个值。但是当你在模拟上调用一个方法时,它不仅会返回一个值,而且还强制要求你设置该方法首先被调用。换句话说,如果设置期望然后不调用该方法,则会抛出异常。当你设定一个期望时,你基本上是在说“如果这个方法没有被调用,那就出错了。”反之亦然,如果你在模拟上调用一个方法并且没有设置期望,它将抛出一个异常,实质上是说“嘿,当你没想到它时,你正在做什么调用这个方法。”

有时你不希望对你所调用的每个方法都有期望,所以一些模拟框架会允许像模拟/存根混合一样的“部分”模拟,因为只有你设定的期望被强制执行,而且每个其他方法都是如此call更像是一个stub,因为它只返回一个值。

使用存根的一个有效位置我可以想到,但是,当您将测试引入遗留代码时。有时通过子类化您正在测试的类而不是重构所有内容以使模拟变得容易甚至可能来创建存根更容易。

对此......

  

避免使用模拟,因为它们会使测试变得脆弱。如果模拟的接口发生变化,您的测试现在对实现调用的方法有了复杂的了解......您的测试会中断。因此,请使用您的最佳判断。<

...我说如果我的界面发生变化,我的测试最好休息一下。因为单元测试的重点在于它们准确地测试我现在存在的代码。

答案 2 :(得分:2)

这取决于您正在进行的测试类型。如果您正在进行基于行为的测试,则可能需要动态模拟,以便验证是否发生了与您的依赖性的某些交互。但是,如果您正在进行基于状态的测试,则可能需要存根,以便验证值/ etc

例如,在下面的测试中,您注意到我存根视图,因此我可以验证属性值是否已设置(基于状态的测试)。然后我创建了一个服务类的动态模拟,这样我就可以确保在测试期间调用一个特定的方法(基于交互/行为的测试)。

<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

答案 3 :(得分:2)

最好使用组合,你必须使用自己的判断。这是我使用的指南:

  • 如果调用外部代码是代码预期(外向)行为的一部分,则应对此进行测试。使用模拟。
  • 如果调用实际上是外部世界不关心的实现细节,则更喜欢存根。但是:
  • 如果您担心测试代码的后续实现可能会意外地绕过您的存根,并且您想要注意是否发生这种情况,请使用模拟。您将测试与代码耦合,但这是为了注意您的存根不再足够,并且您的测试需要重新工作。

第二种模拟是一种必要的邪恶。真正发生的事情是,无论你使用存根还是模拟,在某些情况下,你必须比你想要的更多地耦合你的代码。当发生这种情况时,最好只使用模拟而不是存根,因为您将知道何时该耦合中断并且您的代码不再按照您的测试认为的方式编写。当你这样做时,最好在测试中留下评论,以便无论谁打破它都知道他们的代码没有错,测试就是。

同样,这是代码气味和最后的手段。如果您发现需要经常这样做,请尝试重新考虑编写测试的方式。

答案 4 :(得分:2)

不要介意统计与互动。想想角色和关系。如果一个对象与一个邻居协作以完成其工作,那么该关系(如在界面中表示的)是使用模拟进行测试的候选者。如果对象是具有一些行为的简单值对象,则直接对其进行测试。我无法看到手工编写模拟(甚至是存根)的重点。这就是我们所有人开始和重构的方式。

如需更长时间的讨论,请考虑查看http://www.mockobjects.com/book

答案 5 :(得分:1)

this blog post中阅读Luke Kanies关于这个问题的讨论。他引用a post from Jay Fields甚至暗示使用[相当于ruby's / mocha的] stub_everything更适合使测试更加健壮。引用Fields的最后一句话:“Mocha使定义模拟和定义存根一样容易,但这并不意味着你应该总是喜欢模拟。实际上,我通常更喜欢存根并在必要时使用模拟。 “