关于单元测试:模拟比存根更好吗?

关于单元测试:模拟比存根更好吗?

Are mocks better than stubs?

不久前,我阅读了Martin Fowler的Mocks Are n't Stubs文章,我必须承认我对增加复杂性方面的外部依赖有些担心,所以我想问一下:

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

始终使用模拟框架自动模拟被测试方法的依赖关系会更好吗?还是您希望使用更简单的机制,例如测试存根?


正如口头禅所说的:"去做可能可行的最简单的事情。"

  • 如果伪造的班级可以完成工作,请选择它们。
  • 如果您需要模拟具有多个方法的接口,请使用模拟框架。
  • 始终避免使用模拟程序,因为它们会使测试变脆。现在,如果模拟的接口或您的实现发生更改,您的测试将对实现所调用的方法有复杂的了解...测试将中断。这很糟糕,您将花费更多的时间来运行测试,而不仅仅是运行SUT。测试不应与实施不恰当地紧密联系。
    因此,请使用您的最佳判断。.当使用模拟时,我会更喜欢使用模拟,它可以帮助我省去使用n >> 3方法来编写伪类的更新。

    更新结尾/讨论:
    (感谢Toran Billups提供的模拟测试示例。请参见下文)
    嗨,道格,嗯,我认为我们已经超越了另一场神圣的战争-经典TDDers与Mockist TDDers。我想我属于前者。

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

    由于期望,我通常更喜欢使用模拟。当您在存根上调用返回值的方法时,它通常只会为您提供一个值。但是,当您在模拟对象上调用方法时,它不仅会返回值,而且还使您期望将方法设置为首先被调用。换句话说,如果您设置了期望,然后不调用该方法,则会引发异常。当您设定期望值时,您实际上是在说:"如果未调用此方法,则说明出现问题。"相反,如果您在模拟对象上调用方法但未设置期望,则它将引发异常,本质上说:"嘿,当您不期望它时,您在做什么?"

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

    不过,我可以想到的一个有效的使用存根的地方就是将测试引入旧代码时。有时,通过子类化正在测试的类来进行存根比重构所有内容以简化或什至使模拟变得容易。

    对此...

    Avoid using mocks always because they make tests brittle. Your tests now have intricate knowledge of the methods called by the implementation, if the mocked interface changes... your tests break. So use your best judgment..<

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


    没关系,Statist vs. Interaction。考虑一下角色和关系。如果对象与邻居协作以完成其工作,则该关系(在接口中表示)将成为使用模拟进行测试的候选对象。如果对象是具有某些行为的简单值对象,则直接对其进行测试。我看不到手工编写模拟(甚至存根)的意义。这就是我们所有人从此开始并重构的方式。

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


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

    • 如果调用外部代码是代码预期(向外)行为的一部分,则应对此进行测试。使用模拟。
    • 如果该呼叫确实是外界不关心的实现细节,则最好使用存根。然而:
    • 如果您担心测试代码的后续实现可能会意外地绕到存根周围,并且您想知道是否发生了这种情况,请使用模拟程序。您正在将测试与代码耦合在一起,但这是为了注意到存根不再足够并且测试需要重新进行。

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

    再说一遍,这是代码的味道,也是不得已的方法。如果发现需要经常执行此操作,请尝试重新考虑编写测试的方式。


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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <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

    在这篇博客文章中阅读Luke Kanies对这个问题的讨论。他引用了Jay Fields的一篇文章,该文章甚至建议使用[相当于ruby / mocha的] stub_everything可以使测试更健壮。引用菲尔兹的最后一句话:" Mocha使定义模拟与定义存根一样容易,但这并不意味着您应该始终喜欢模拟。实际上,我通常更喜欢存根,并在必要时使用模拟。"


    推荐阅读