【问题标题】:TDD Mocking - Is specifying mock object behaviour white box testing?TDD 模拟 - 是否指定模拟对象行为白盒测试?
【发布时间】:2010-02-18 23:58:19
【问题描述】:

我最近真的开始接触 TDD,在阅读了 Kent Beck 关于测试驱动开发的书之后,我仍然有很多关于测试设计的问题。

我目前遇到的一个问题是使用 Mock 对象。下面以一个非常简单的生成报告为例:

public string MakeFinancialReport()
{
    return sys1.GetData() + sys2.GetData() + sys3.GetData();
}

报告必须包含页眉、正文和页脚。所以快速测试一下报告中是否存在这些标题:

public void TestReport()
{
    string report = MakeFinancialReport();
    Assert.IsTrue(report.Contains("[Title]") && report.Contains("[Body]") && report.Contains("[Footer]"));
 }

为了隔离该方法,我想我会模拟掉 sys1、sys2 和 sys3 调用。现在,如果它们都是模拟物,我还有什么要测试的?此外,当我模拟它们时,为什么我必须告诉模拟对象它们将被调用一次并返回 X 等。它是否不仅仅是一个黑盒测试,并且 MakeFinancialReport 可以进行尽可能多的调用它想建立报告?

我对这么一个小问题感到困惑,我不确定我错过了什么。我认为 Mocking 会带走可测试的代码,对于大多数简单的方法来说,剩下的测试没有任何帮助。

【问题讨论】:

    标签: tdd mocking


    【解决方案1】:

    Martin,我认为你应该为 sys1-3 使用 mock,但它们只需要简单到每个都返回一个字符串即可。

    这意味着您的测试应该如下所示:

    public void TestReport()
    {
        // Setup mocks for sys1-3
        string report = MakeFinancialReport();
        Assert.IsTrue(report.equals("abc"));
    }
    

    这表明 MakeFinancialReport 具有它从 sys1-3 调用 GetData() 的属性并且它以这个特定的顺序连接结果。

    【讨论】:

    • +1 很好的答案。如果每个人都清楚地使用 mocks,它们会被更广泛地接受。
    • @quamrana 我们在这里进行白盒测试吗?毕竟,我们需要知道MakeFinancialReport 的实现才能编写这个测试。如果有一天我们需要添加sys4,我们将不得不同时修改MakeFinancialReport 和测试。这是否意味着使用模拟会损害灵活性?
    • @Satoru.Logic 我们不在这里进行白盒测试 - 请参阅tdd 标签。如果有一天我们写了一个sys4,那么我们将需要一个新的测试来测试一个真正的sys4,并修改我上面的测试来测试MakeFinancialReport 做了一个新的事情。不同的职责在不同的测试中。
    【解决方案2】:

    就目前而言,MakeFinancialReport 除了与更有趣的合作者互动之外几乎没有做任何事情,可能不值得进行单元测试。

    如果我为该方法编写任何单元测试,我可能只是验证该方法在其协作者返回 null 时是否符合我的预期,主要是为了记录预期的行为(或帮助我决定预期的行为如果我提前做)。目前该方法将失败。这可能没问题,但值得考虑是否要将 null 视为空字符串 - 并且单元测试证明您决定的任何行为都是故意的。

    “是否指定模拟对象行为白盒测试?”绝对 - 如果您的班级有一个您正在模拟的依赖项,那么您就是将您的测试与该依赖项联系起来。但是白盒测试有它的优势。并非所有协作者的交互都像您的示例中那样微不足道。

    【讨论】:

      【解决方案3】:

      您应该只在有用的情况下使用模拟对象。如果MakeFinancialReportsys1sys2sys3 都包含复杂的逻辑,那么您需要独立测试它们中的每一个。通过将三个sysX 对象的模拟版本提供给MakeFinancialReport,您可以消除它们的复杂性,只需测试MakeFinancialReport

      当您想要测试错误条件和异常处理时,Mock 对象特别有用,因为可能很难从真实对象中强制异常。

      当您谈到不必设置明确的期望和返回值时,这是一个称为存根的相关概念。您可能会发现 Martin Fowler 的“Mocks Aren't Stubs”很有帮助。

      Kent Beck 的书是一个很好的介绍,但如果您正在寻找更多详细信息,我强烈推荐 xUnit Patterns book。例如,它有一个关于mock objects 的部分,以及更一般的类别test doubles

      【讨论】:

      • 嗯,那我想我在某处缺少测试设计原则。我认为单元测试是为了测试一种方法,即一小段离散代码,而不是它的任何协作对象。在这种情况下,协作对象 sys1、sys2 和 sys3,每个都有自己的一组单元测试,分别包含在单独的单元测试中。我读过很多次 Martin Fowler 的文章,从 Mocks 和 Stubs 的差异中我可以看出状态和行为风格测试之间的差异。
      • 我仍然需要了解我们之所以使用它们是因为我们试图摆脱在方法/单元之外调用代码的复杂性,以便我们可以专注于一种方法的行为。那么在这种情况下,如果我们通过 mocks 拿走了 3 个系统调用,eMakeFinancialReport() 方法中究竟还剩下什么?
      • 在你的例子中,@Martin,剩下的就是字符串连接。这没什么好测试的。如果没有什么要测试的,那么这就是一种代码味道,会促使您进行“内联方法”重构。也许你应该寻找一个更好的例子。
      • 我同意这是一个糟糕的例子,但我现在能想到的最好的例子;)你是否暗示代码味道是方法太简单,不值得测试?
      【解决方案4】:

      我认为其中一个问题是您的测试将 sys1、sys2 和 sys3 的职责与 TestReport 方法的职责混合在一起。在我看来,您应该将测试分为两部分:

      1) MakeFinancialReport() 返回 sys1、sys2、sys3 的串联。在那里你可以存根 sys1 等......,类似于

      var sys1 =MockRepository.GenerateStub<ISys>();
      sys1.Expect(s=>s.GetData()).Return("Part 1");
      // etc... for sys2, sys3 var
      reportMaker = new ReportMaker(sys1,sys2, sys3); 
      Assert.AreEqual("Part 1" + "Part 2" + "Part 3", reportMaker.MakeFinancialReport();
      

      拥有 MakeFinancialReport() 方法的类不应该关心或知道 sys 类在做什么。他们可以返回任何类 - MakeFinancialReport() 只是连接,这就是你应该测试的(如果你认为值得的话)。

      2) 从接口 sys1、sys2、sys3 实现测试 GetData() 方法。您可能会在此处检查您希望看到“正文”、“标题”等的情况...
      存根在这里可能有点矫枉过正,但这给你带来的是一个潜在的重依赖(3 个 sys 实例)的廉价实例化,并且明确区分了 sys 的作用和 MakeFinancialReport 的作用。

      顺便说一句,这可能与您使用的语言有关,但令人惊讶的是,您的测试并未从拥有 MakeFinancialReport() 的类的实例化开始。

      【讨论】:

        猜你喜欢
        • 2010-12-12
        • 2012-08-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多