不久前我读到了 模拟不是存根 Martin Fowler 的文章,我必须承认我有点害怕外部依赖性增加的复杂性,所以我想问:

单元测试时最好使用什么方法?

始终使用模拟框架来自动模拟正在测试的方法的依赖关系是否更好,或者您更喜欢使用更简单的机制(例如测试存根)?

有帮助吗?

解决方案

正如口头禅所说:“用最简单、可行的方法。”

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

避免使用模拟 总是 因为它们使测试变得脆弱。如果模拟的接口或您的实现发生变化,您的测试现在对实现调用的方法有了复杂的了解......你的测试失败了。这很糟糕,因为您将花费额外的时间来运行测试,而不仅仅是让 SUT 运行。 测试不应与实现过于密切。
所以用你最好的判断..我更喜欢模拟,因为它可以帮助我节省用 n>>3 方法编写更新假类的时间。

更新 尾声/审议:
(感谢 Toran Billups 提供的模拟测试示例。见下文)
嗨,Doug,我认为我们已经进入了另一场圣战 - 经典 TDD 玩家 vs Mockist TDD 玩家。我想我属于前者。

  • 如果我在 test#101 Test_ExportProductList 上,我发现我需要向 IProductService.GetProducts() 添加一个新参数。我这样做让这个测试绿色。我使用重构工具来更新所有其他引用。现在我发现所有调用该成员的模拟测试现在都崩溃了。然后我必须回去更新所有这些测试——浪费时间。为什么 ShouldPopulateProductsListOnViewLoadWhenPostBackIsFalse 失败?是因为代码被破坏了吗?相反,测试被破坏了。我赞成 一次测试失败 = 1 个需要修复的地方. 。嘲笑频率与此相反。存根会更好吗?如果我有一个 fake_class.GetProducts()..确保更改一个地方,而不是通过多个 Expect 调用进行霰弹枪手术。归根结底还是风格问题。。如果您有一个通用的实用方法 MockHelper.SetupExpectForGetProducts() - 那也足够了..但你会发现这并不常见。
  • 如果在测试名称上放置白色条带,则测试将难以阅读。模拟框架的大量管道代码隐藏了正在执行的实际测试。
  • 要求您学习模拟框架的这种特殊风格

其他提示

由于期望,我通常更喜欢使用模拟。当您在存根上调用返回值的方法时,它通常只返回一个值。但是,当您在模拟上调用方法时,它不仅会返回一个值,还会强制您设置该方法甚至首先被调用的期望。换句话说,如果您设置了期望,然后不调用该方法,则会引发异常。当您设定期望时,您实际上是在说:“如果这种方法未被调用,则出现问题。”相反,如果您在模拟中调用一种方法而没有设定期望,它将抛出一个例外,从本质上说:“嘿,当您没有期待时,您在做什么。”

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

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

而对此...

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

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

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

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

第二种嘲笑是一种必要之恶。实际上,这里发生的事情是,无论您使用存根还是模拟,在某些情况下,您都必须比您想要的更多地耦合到代码。当发生这种情况时,最好使用模拟而不是存根,因为您会知道耦合何时破裂并且您的代码不再按照测试认为的方式编写。执行此操作时,最好在测试中留下注释,以便破坏它的人知道他们的代码没有错,测试才是错误的。

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

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

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

<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

别介意 Statist vs.相互作用。思考角色和关系。如果一个对象与邻居协作来完成其工作,那么该关系(如接口中所表达的)就是使用模拟进行测试的候选者。如果一个对象是一个具有一些行为的简单值对象,那么直接测试它。我看不出手工编写模拟(甚至存根)的意义。这就是我们开始并重构的方式。

对于更长的讨论,请考虑看一下 http://www.mockobjects.com/book

阅读 Luke Kanies 对这个问题的讨论 这篇博文. 。他引用 杰·菲尔兹的帖子 这甚至表明最好使用[与 ruby​​'s/mocha's 相当的]stub_everything 来使测试更加健壮。引用菲尔兹的最后一句话:“Mocha 使得定义模拟就像定义存根一样容易,但这并不意味着您应该总是更喜欢模拟。事实上,我通常更喜欢存根并在必要时使用模拟。”

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top