14

不久前,我阅读了Martin Fowler 的Mocks Aren't Stubs文章,我必须承认我有点害怕外部依赖会增加复杂性,所以我想问一下:

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

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

4

6 回答 6

12

正如口头禅所说的那样,“去做最简单的事情,可能会奏效。”

  1. 如果假课程可以完成工作,那就去吧。
  2. 如果您需要一个具有多个模拟方法的接口,请使用模拟框架。

始终避免使用模拟,因为它们会使测试变得脆弱。您的测试现在对实现调用的方法有了复杂的了解,如果模拟接口或您的实现发生变化……您的测试会中断。这很糟糕,因为您将花费额外的时间让您的测试运行,而不仅仅是让您的 SUT 运行。测试不应不恰当地与实现密切相关。
所以用你最好的判断..我更喜欢模拟,因为它可以帮助我用 n>>3 方法更新一个假类。

更新结语/
深思熟虑:(感谢 Toran Billups 以模拟测试为例。见下文)
嗨,道格,我认为我们已经超越了另一场圣战 - 经典 TDDers 与 Mockist TDDers。我想我属于前者。

  • 如果我在 test#101 Test_ExportProductList 上并且我发现我需要向 IProductService.GetProducts() 添加一个新参数。我这样做会使这个测试变绿。我使用重构工具来更新所有其他引用。现在我发现所有调用这个成员的 mockist 测试现在都崩溃了。然后我必须回去更新所有这些测试——浪费时间。为什么 ShouldPopulateProductsListOnViewLoadWhenPostBackIsFalse 失败?是因为密码坏了吗?而是测试被破坏了。我赞成一个测试失败= 1个地方修复. 模拟频率与此相反。存根会更好吗?如果它我有一个 fake_class.GetProducts().. 肯定一个地方可以改变,而不是在多个期望调用中进行霰弹枪手术。最后,这是一个风格问题。如果您有一个通用的实用程序方法 MockHelper.SetupExpectForGetProducts() - 这也足够了.. 但您会发现这并不常见。
  • 如果您在测试名称上放置白色条带,则测试很难阅读。模拟框架的大量管道代码隐藏了正在执行的实际测试。
  • 要求您学习这种特殊风格的模拟框架
于 2008-09-06T20:00:23.650 回答
8

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

有时您不希望对您调用的每个方法都有期望,因此一些模拟框架将允许类似于模拟/存根混合体的“部分”模拟,因为只有您设置的期望被强制执行,并且所有其他方法调用都被处理更像是一个存根,因为它只返回一个值。

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

而对此...

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

...我说如果我的界面发生变化,我的测试最好中断。因为单元测试的全部意义在于它们准确地测试了我现在存在的代码。

于 2008-09-07T00:16:48.747 回答
2

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

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

<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
于 2008-09-06T20:05:42.623 回答
2

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

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

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

再说一次,这是一种代码味道,也是最后的手段。如果您发现需要经常这样做,请尝试重新考虑编写测试的方式。

于 2008-09-06T20:12:31.703 回答
2

别介意统计学家与互动。想想角色和关系。如果一个对象与邻居合作完成它的工作,那么这种关系(在接口中表示)是使用模拟测试的候选对象。如果一个对象是一个有一点行为的简单值对象,那么直接测试它。我看不出手动编写模拟(甚至存根)的意义。这就是我们所有人开始和重构的方式。

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

于 2009-05-21T18:33:07.163 回答
1

在这篇博文中阅读 Luke Kanies 对这个问题的讨论。他引用了 Jay Fields 的一篇文章,该文章甚至建议使用 [a 等价于 ruby​​'s/mocha's] stub_everything 以使测试更加健壮。引用 Fields 的最后一句话:“Mocha 让定义模拟就像定义存根一样容易,但这并不意味着你应该总是更喜欢模拟。事实上,我通常更喜欢存根并在必要时使用模拟。 "

于 2008-09-06T19:37:24.940 回答