Unit testing 模仿比存根好吗?

Unit testing 模仿比存根好吗?,unit-testing,mocking,Unit Testing,Mocking,不久前,我读了Martin Fowler的文章,我必须承认我有点害怕增加复杂性的外部依赖,所以我想问: 单元测试时使用的最佳方法是什么 始终使用模拟框架来自动模拟被测试方法的依赖关系更好吗,还是更喜欢使用更简单的机制,例如测试存根?阅读Luke Kanies在中对这个问题的讨论。他引用了一篇文章,其中甚至建议使用[相当于ruby/mocha的]stub_,一切都可以让测试更加健壮。引用Fields的最后一句话:“Mocha让定义一个mock和定义一个存根一样容易,但这并不意味着你应该总是喜欢mo

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

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


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

阅读Luke Kanies在中对这个问题的讨论。他引用了一篇文章,其中甚至建议使用[相当于ruby/mocha的]stub_,一切都可以让测试更加健壮。引用Fields的最后一句话:“Mocha让定义一个mock和定义一个存根一样容易,但这并不意味着你应该总是喜欢mock。事实上,我通常更喜欢存根,并在必要时使用mock。”

正如格言所说的那样“用可能工作的最简单的东西。”

  • 如果假课能完成任务,那就跟着他们去吧
  • 如果需要模拟具有多个方法的接口,请使用模拟框架
  • 避免总是使用模拟,因为它们会使测试变得脆弱。如果模拟接口或您的实现发生更改,您的测试现在对实现调用的方法有了复杂的了解。。。你的测试失败了。这很糟糕,因为您将花费额外的时间来运行测试,而不仅仅是运行SUT。测试不应与实现过分亲密。
    所以,用你最好的判断。。我更喜欢mock,因为它可以帮助我用n>>3个方法更新一个假类

    更新结语/审议:
    (感谢Toran Billups的模拟测试示例。见下文)
    嗨,道格,我想我们已经进入了另一场圣战——经典的TDDers vs Mockist TDDers。我想我属于前者

    • 如果我在test#101 test#u ExportProductList上,我发现需要向ipProductService.GetProducts()添加一个新参数。我这样做,让这个测试变成绿色。我使用重构工具来更新所有其他引用。现在我发现所有调用这个成员的模拟测试都失败了。然后我必须回去更新所有这些测试——这是浪费时间。当返回为false失败时,为什么应该加载PopulateProductsListViewLoad?是因为密码坏了吗?相反,测试失败了。我赞成一个测试失败=一个需要修复的地方。嘲笑freq与此相反。存根会更好吗?如果我有一个假的类。GetProducts()。。确保有一个地方可以改变,而不是在多个Expect电话中进行枪战手术。归根结底,这是一个风格问题。。如果您有一个通用的实用程序方法MockHelper.SetupExpectForGetProducts()-那就足够了。。但你会发现这很少见
    • 如果在测试名称上放置白色条带,则测试很难读取。模拟框架的大量管道代码隐藏了正在执行的实际测试
    • 需要您学习模拟框架的这种特殊风格

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

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

    _
    当“回发”为“假”()时,公共子项应“填充”产品“列表”视图“加载”
    mMockery=newmockrepository()
    mView=DirectCast(mMockery.Stub(属于IPProductView)(),IPProductView)
    MPProductService=DirectCast(mMockery.DynamicMock(属于IPProductService)(),IPProductService)
    MPPresenter=新产品演示者(mView、MPProductService)
    Dim ProductList作为新列表(产品)()
    ProductList.Add(新产品())
    使用mMockery.Record()
    SetupResult.For(mView.PageIsPostBack).返回(False)
    Expect.Call(mpProductService.GetProducts()).Return(ProductList).Repeat.Once()
    终端使用
    使用mMockery.Playback()
    mPresenter.OnViewLoad()文件
    终端使用
    '当回发为false时,验证我们在方法期间是否命中了服务依赖项
    Assert.AreEqual(1,mView.Products.Count)
    mMockery.VerifyAll()
    端接头
    
    最好使用组合,您必须使用自己的判断。以下是我使用的指导原则:

    • 如果调用外部代码是您的代码预期(外向)行为的一部分,那么应该对此进行测试。使用一个模拟
    • 如果调用实际上是一个外部世界不关心的实现细节,那么最好使用存根。然而:
    • 如果您担心测试代码的后续实现可能会意外地绕过存根,并且希望注意到这种情况,请使用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