【问题标题】:How do you unit test a unit test? [closed]如何对单元测试进行单元测试? [关闭]
【发布时间】:2008-10-28 18:37:05
【问题描述】:

我在 MVCStoreFront 应用程序上观看 Rob Connerys 的网络广播,我注意到他正在对最平凡的事情进行单元测试,例如:

public Decimal DiscountPrice
{
   get
   {
       return this.Price - this.Discount;
   }
}

会有这样的测试:

[TestMethod]
public void Test_DiscountPrice
{
    Product p = new Product();
    p.Price = 100;
    p.Discount = 20;
    Assert.IsEqual(p.DiscountPrice,80);
}

虽然我完全支持单元测试,但有时我想知道这种形式的测试优先开发是否真的有益,例如,在实际流程中,您的代码上方有 3-4 层(业务请求、需求文档、架构文档),其中实际定义的业务规则(折扣价格是价格 - 折扣)可能被错误定义。

如果是这种情况,您的单元测试对您来说毫无意义。

此外,您的单元测试是另一个失败点:

[TestMethod]
public void Test_DiscountPrice
{
    Product p = new Product();
    p.Price = 100;
    p.Discount = 20;
    Assert.IsEqual(p.DiscountPrice,90);
}

现在测试有缺陷。显然在一个简单的测试中,这没什么大不了的,但是假设我们正在测试一个复杂的业务规则。我们在这里有什么收获?

应用程序生命周期快进两年,此时维护开发人员正在对其进行维护。现在业务改变了它的规则,测试又中断了,一些菜鸟开发者错误地修复了测试……我们现在又遇到了一个故障点。

我看到的都是更多可能的失败点,没有真正的收益回报,如果折扣价格错误,测试团队仍然会发现问题,单元测试如何节省任何工作?

我在这里缺少什么?请教我热爱 TDD,因为到目前为止我很难接受它是有用的。我也想,因为我想保持进步,但这对我来说没有意义。

编辑:有几个人一直提到测试有助于执行规范。根据我的经验,规范也经常出错,但也许我注定要在一个规范由不应该编写规范的人编写的组织工作。

【问题讨论】:

  • 在许多情况下,单元测试规范和文档!
  • ……然后单元测试单元测试的单元测试……但是unit^4测试和unit^5测试呢……aaaaaaaaahhhhhhhhh!
  • 再多的任何类型的测试都不会让您避免错误的规范。
  • 有没有其他人听到像一只手拍手的声音?
  • 我认为合适的引用是“这只是海龟一路下来。”

标签: tdd agile test-first


【解决方案1】:

首先,测试就像安全性一样——你永远无法 100% 确定你已经掌握了它,但每一层都增加了更多的信心和一个框架,可以更轻松地解决仍然存在的问题。

其次,您可以将测试分解为子例程,然后对其本身进行测试。当您有 20 个类似的测试时,制作一个(经过测试的)子例程意味着您的主要测试是对子例程的 20 次简单调用,这更有可能是正确的。

第三,有些人认为TDD 解决了这个问题。也就是说,如果你只写了 20 个测试并且它们通过了,你就不能完全确信它们实际上正在测试任何东西。但是,如果您最初编写的每个测试失败,然后您修复了它,那么您就会更有信心它确实在测试您的代码。恕我直言,这种反复来回花费的时间比它的价值要多,但这是一个试图解决您问题的过程。

【讨论】:

  • 为了扮演魔鬼的拥护者,我认为额外的层是更多可能的失败点,这不会增加我的信心。在我的实际工作中,我与高度分布式 SOA 企业的许多团队一起工作。如果他们的层失败,每个团队都可能危及项目。
  • 这就是您使用模拟对象分别测试每一层的原因。
  • 太好了,所以我的测试将通过使用 IMockedComplexObject 但当我在现实世界中实际使用 ComplexObject 时,它失败了......我又什么也没得到。
  • @Jonathan -- 不,您已经确信您的代码可以正常工作 -- 假设您开发了 ComplexObject 的接口并针对该接口进行了充分测试。最坏的情况是,您了解到您对 ComplexObject 的理解并非您的预期。
  • @FlySwat:在回应您对 IMockedComplexObject 的评论时,我引用了 Cwash 对另一个答案的评论:“您不会模拟您要测试的内容,而是要模拟您不想尝试的内容测试。”
【解决方案2】:

测试错误不太可能破坏您的生产代码。至少,不比没有考试更糟糕。所以这不是一个“故障点”:为了让产品实际工作,测试不必是正确的。它们可能必须是正确的,然后才能生效,但修复任何损坏的测试的过程不会危及您的实现代码。

您可以将测试,甚至像这样的微不足道的测试视为代码应该做什么的第二意见。一种意见是测试,另一种是实施。如果他们不同意,那么你知道你有问题,你仔细看看。

如果将来有人想从头开始实现相同的接口,它也很有用。他们不必阅读第一个实现即可了解 Discount 的含义,并且测试充当您可能拥有的任何书面接口描述的明确备份。

也就是说,您正在权衡时间。如果您可以使用跳过这些琐碎测试所节省的时间来编写其他测试,那么它们可能会更有价值。这实际上取决于您的测试设置和应用程序的性质。如果折扣对应用程序很重要,那么无论如何您都会在功能测试中发现此方法中的任何错误。所有单元测试所做的就是让您在测试该单元时捕获它们,此时错误的位置将立即显而易见,而不是等到应用程序集成在一起并且错误的位置可能 不那么明显。

顺便说一句,我个人不会在测试用例中使用 100 作为价格(或者更确切地说,如果我这样做了,那么我会添加另一个价格的测试)。原因是将来有人可能认为折扣应该是百分比。像这样的琐碎测试的一个目的是确保纠正阅读规范时的错误。

[关于编辑:我认为不正确的规范不可避免地会成为故障点。如果您不知道该应用程序应该做什么,那么它很可能不会这样做。但是编写测试来反映规范并没有放大这个问题,它只是无法解决它。所以你没有添加新的故障点,你只是在代码中表示现有的错误而不是 waffle 文档。]

【讨论】:

  • 错误的测试会让损坏的代码泛滥成灾。这就是引入失败的地方。它提供了一种虚假的自信感。
  • 确实如此,但没有测试也会导致代码损坏。错误是认为如果代码通过了单元测试,它一定是正确的——我在职业生涯的早期就被治愈了。因此,一个损坏的单元测试不会让损坏的代码进入野外,它只会让它进入集成测试。
  • 此外,即使是错误的测试也可以捕获损坏的代码,只要它包含与实现不同的错误。这就是我的观点,测试并不一定要正确,它们的存在是为了将您的注意力吸引到关注的领域。
  • 非常有趣的答案。
【解决方案3】:

我看到的都是更多可能的失败点,没有真正的收益回报,如果折扣价格错误,测试团队仍然会发现问题,单元测试如何节省任何工作?

单元测试并不是真正应该节省工作,它应该帮助您发现和防止错误。这是更多的工作,但它是正确的工作。它在最低粒度级别考虑您的代码,并编写测试用例来证明它在预期条件下工作,对于给定的一组输入。它隔离了变量,因此您可以在出现错误时找到正确的位置来节省时间。它保存那套测试,这样当你必须在路上做出改变时,你可以一次又一次地使用它们。

我个人认为大多数方法都没有从cargo cult software engineering 中删除多少步骤,包括 TDD,但您不必遵守严格的 TDD 即可获得单元测试的好处。保留好的部分,丢弃没有收益的部分。

最后,您的题为“如何对单元测试进行单元测试?”的答案是您不必这样做。每个单元测试都应该非常简单。调用具有特定输入的方法并将其与预期输出进行比较。如果方法的规范发生更改,那么您可以预期该方法的某些单元测试也需要更改。这就是您在如此低的粒度级别上进行单元测试的原因之一,因此只有 一些 单元测试需要更改。如果您发现针对某项需求的一项更改,许多不同方法的测试都发生了变化,那么您可能没有在足够精细的粒度级别上进行测试。

【讨论】:

  • "调用具有特定输入的方法并将其与预期输出进行比较。"但是如果输出是一个复杂的类型……就像一个 XML 文档。你不能只是“==”,你必须编写特定的代码比较,然后你的比较方法可能有问题??
  • @andy:你必须单独测试你的比较方法。一旦你彻底测试了它,你就可以依赖它在其他测试中工作。
  • 酷,谢谢比尔。我开始在一个新地方工作,这是我第一次使用单元测试。我认为原则上它是有效的,我们正在使用巡航控制,它真的很有用,但是大量的测试似乎和遗留代码一样遭受同样的命运......我只是不确定......跨度>
【解决方案4】:

单元测试可以让您的单元(方法)达到您的预期。首先编写测试会迫使您考虑在编写代码之前您的期望。三思而后行总是一个好主意。

单元测试应该反映业务规则。当然,代码中可能存在错误,但首先编写测试允许您在编写任何代码之前从业务规则的角度编写它。我认为,之后编写测试更有可能导致您描述的错误,因为您知道代码如何实现它,并且很想确保实现正确 - 而不是意图是正确的。

此外,单元测试只是您应该编写的测试的一种形式——也是最低的一种形式。集成测试和验收测试也应该由客户编写,如果可能的话,以确保系统按照预期的方式运行。如果您在此测试期间发现错误,请返回并编写单元测试(失败)以测试功能更改以使其正常工作,然后更改您的代码以使测试通过。现在你有了回归测试来捕获你的错误修复。

[编辑]

我在做 TDD 时发现的另一件事。默认情况下,它几乎强制进行良好的设计。这是因为高度耦合的设计几乎不可能单独进行单元测试。使用 TDD 很快就会发现使用接口、控制反转和依赖注入(所有可以改进设计和减少耦合的模式)对于可测试代码非常重要。

【讨论】:

  • 也许这就是我的问题所在。与可视化结果相比,我可以更轻松地可视化业务规则的算法,因此实现代码本身没有问题,但将模拟规则视为多余。也许这正是我的想法。
  • 这正是您在单元测试中所做的。将该算法分解成小块并检查每一块。通常,我发现我的代码会自己编写,因为我已经在单元测试中编写了期望。
  • Mock 在测试领域是一个重载的术语。您不会模拟您要测试的内容,而是要模拟您不想测试的内容......当您为业务规则编写测试时,您会创建调用它的代码 - 它根本不是在模拟它.
  • @cwash -- 我不确定您的评论如何适用于我的回答。我没有提到嘲笑......我同意你的观察。
  • @tvanfosson - 我的最后一条评论是对@FlySwat “...嘲笑规则是多余的”的回应。抱歉,我忘记指定了。
【解决方案5】:

test a test 怎么样? Mutation testing 是我个人使用的一种有价值的技术,效果非常好。阅读链接的文章了解更多详细信息,以及更多学术参考的链接,但通常它通过修改源代码(例如将“x += 1”更改为“x -= 1”)“测试你的测试”,然后重新运行测试,确保至少一项测试失败。任何不会导致测试失败的突变都会被标记以供以后调查。

您会惊讶于如何通过一组看起来很全面的测试实现 100% 的行和分支覆盖率,而且您可以从根本上更改甚至注释掉源代码中的一行,而不会引起任何测试的抱怨。这通常归结为没有使用正确的输入进行测试以涵盖所有边界情况,有时它更微妙,但在所有情况下,我都对它的成果印象深刻。

【讨论】:

  • +1 个我还没有听说过的有趣概念
【解决方案6】:

在应用测试驱动开发 (TDD) 时,首先要进行失败测试。这一步看似不必要,实际上是为了验证单元测试是否正在测试某些东西。事实上,如果测试永远不会失败,它不会带来任何价值,更糟糕的是,会导致错误的信心,因为您将依赖无法证明任何事情的积极结果。

当严格遵循这个过程时,所有“单元”都受到单元测试正在建立的安全网的保护,即使是最普通的也是如此。

Assert.IsEqual(p.DiscountPrice,90);

测试没有理由朝那个方向发展——或者我在你的推理中遗漏了一些东西。当价格为 100,折扣 20 时,折扣价格为 80。这就像一个不变量。

现在假设您的软件需要支持另一种基于百分比的折扣,可能取决于购买的数量,您的 Product::DiscountPrice() 方法可能会变得更加复杂。引入这些更改可能会破坏我们最初的简单折扣规则。然后你会看到这个测试的值,它会立即检测到回归。


Red - Green - Refactor - 这是为了记住 TDD 过程的本质。

红色是指测试失败时的 JUnit 红色条。

绿色是所有测试通过时JUnit进度条的颜色。

重构在绿色条件下:删除任何重复,提高可读性。


现在要解决您关于“代码上方的 3-4 层”的观点,这在传统(类似瀑布)过程中是正确的,而不是在开发过程敏捷时。敏捷是 TDD 的发源地; TDD 是eXtreme Programming 的基石。

敏捷是关于直接沟通,而不是扔在墙上的需求文档。

【讨论】:

    【解决方案7】:

    虽然我完全支持单元测试,但我 有时想知道这种形式的测试是否 第一次开发真是受益匪浅……

    像这样的小而琐碎的测试可以成为您代码库的“煤矿中的金丝雀”,在为时已晚之前提醒危险。琐碎的测试对于保留是有用的,因为它们可以帮助您正确进行交互。

    例如,考虑一个简单的测试来探究如何使用您不熟悉的 API。如果该测试与您在“真正”使用 API 的代码中所做的事情有任何相关性,那么保留该测试很有用。当 API 发布新版本并且您需要升级时。现在,您对 API 的行为方式有了自己的假设,并以可执行格式记录下来,您可以使用该格式来捕获回归。

    ...[I]在一个真实的过程中,你有 3-4 代码上方的层(业务 请求,要求文件, 架构文档),其中 实际定义的业务规则(折扣 价格是价格 - 折扣)可能是 定义错误。如果是这种情况, 你的单元测试对你没有任何意义。

    如果您多年来一直在编写代码而没有编写测试,那么您可能不会立即明白其中有什么价值。但是,如果您认为最好的工作方式是“尽早发布,经常发布”或“敏捷”,因为您希望能够快速/持续地部署,那么您的测试肯定是有意义的。做到这一点的唯一方法是通过测试使您对代码所做的每一次更改都合法化。无论测试多么小,一旦您拥有一个绿色测试套件,理论上您就可以部署。另见“持续生产”和“永久测试版”。

    您也不必“先测试”才能拥有这种心态,但这通常是实现这一目标的最有效方式。当您进行 TDD 时,您会将自己锁定在 2 到 3 分钟的小红绿重构周期中。在任何时候,您都不能停下来离开,手上会弄得一团糟,需要一个小时才能调试并重新组装起来。

    此外,您的单元测试是另一个 故障点...

    成功的测试是证明系统出现故障的测试。失败的测试会提醒您测试逻辑或系统逻辑中的错误。您的测试目标是破坏您的代码或证明一种方案有效。

    如果您代码之后编写测试,您将冒着编写“糟糕”测试的风险,因为为了看到您的测试真正有效,您需要同时看到它破碎和工作。当您在代码之后编写测试时,这意味着您必须“跳出陷阱”并在代码中引入错误以查看测试失败。大多数开发人员不仅对此感到不安,而且认为这是浪费时间。

    我们在这里得到了什么?

    以这种方式做事肯定有好处。 Michael Feathers 将“遗留代码”定义为“未经测试的代码”。当您采用这种方法时,您对代码库所做的每一次更改都是合法的。它比不使用测试更严格,但是在维护大型代码库时,它会为自己付出代价。

    说到 Feathers,您应该查看两个很好的资源:

    这两者都解释了如何将这些类型的实践和学科运用到非“新建”项目中。它们提供了围绕紧密耦合的组件、硬连线依赖以及您不一定可以控制的事物编写测试的技术。一切都是为了找到“接缝”并围绕这些进行测试。

    [I]如果折扣价格错误,则 测试团队仍然会发现问题, 单元测试如何节省任何工作?

    像这样的习惯就像是一种投资。退货不是即时的;它们随着时间的推移而积累。不测试的替代方法本质上是承担无法捕捉回归、引入代码而不用担心集成错误或驱动设计决策的债务。美妙之处在于您将引入代码库的每项更改都合法化。

    我在这里缺少什么?请教 我喜欢 TDD,因为我很难 时间接受它到目前为止有用。一世 也想,因为我想留下 进步的,但它并没有使 对我有感觉。

    我将其视为一种职业责任。是一个为之奋斗的理想。但这非常难以理解和乏味。如果您关心它,并且觉得您不应该编写未经测试的代码,您将能够找到学习良好测试习惯的意志力。我现在(和其他人一样)经常做的一件事是给自己一个小时的时间来编写代码,而无需任何测试,然后有纪律地扔掉它。这可能看起来很浪费,但事实并非如此。这不像锻炼会花费公司的物质材料。它帮助我理解了问题以及如何以更高质量和可测试的方式编写代码。

    我的最终建议是,如果你真的不想擅长它,那就不要去做。没有维护的糟糕测试,表现不佳等可能比没有任何测试更糟糕。自己学习很难,你可能不会喜欢它,但如果你没有学习的欲望,或者看不到它的足够价值,那么它几乎是不可能学习的。保证时间投入。

    有几个人一直提到 测试有助于执行规范。它有 是我对规范的经验 也犯过错,比 不是……

    开发人员的键盘是橡胶与道路相遇的地方。如果规范是错误的并且你没有在上面举起旗帜,那么你很可能会因此受到指责。或者至少你的代码会。测试中涉及的纪律和严格性很难遵守。这一点都不容易。这需要练习、大量学习和大量错误。但最终它确实得到了回报。在一个快节奏、快速变化的项目中,这是您晚上睡觉的唯一方法,无论它是否会让您慢下来。

    这里要考虑的另一件事是,与测试基本相同的技术在过去已被证明是有效的:“洁净室”和“合同设计”都倾向于产生相同类型的“元”-测试所做的代码构造,并在不同的点执行这些构造。这些技术都不是灵丹妙药,严格性最终会让您在可以交付的功能范围内付出代价,以缩短上市时间。但这不是它的目的。这是关于能够维持你所交付的东西。这对于大多数项目来说非常重要。

    【讨论】:

      【解决方案8】:

      单元测试的工作方式与复式簿记非常相似。您以两种完全不同的方式陈述同一事物(业务规则)(作为生产代码中的编程规则,以及作为测试中的简单、有代表性的示例)。您不太可能在两者中犯相同的错误,因此如果他们都彼此同意,那么您不太可能出错。

      测试如何值得付出努力?根据我的经验,至少有四种方式,至少在进行测试驱动开发时:

      • 它可以帮助您提出良好的解耦设计。您只能对解耦良好的代码进行单元测试;
      • 它可以帮助您确定何时完成。必须在测试中指定所需的行为有助于不构建您实际上不需要的功能,并确定功能何时完成;
      • 它为重构提供了一个安全网,使代码更易于更改;和
      • 它可以为您节省大量调试时间,但代价高昂(据我所知,传统上,开发人员将 80% 的时间用于调试)。

      【讨论】:

        【解决方案9】:

        大多数单元测试,测试假设。在这种情况下,折扣价格应该是价格减去折扣。如果您的假设是错误的,我敢打赌您的代码也是错误的。如果你犯了一个愚蠢的错误,测试就会失败,你会改正它。

        如果规则改变,测试将失败,这是一件好事。所以在这种情况下你也必须改变测试。

        作为一般规则,如果测试立即失败(并且您没有使用测试优先设计),则说明测试或代码有误(或者如果您的日子不好过,两者都有)。您使用常识(并且可能根据规范)来纠正有问题的代码并重新运行测试。

        就像 Jason 所说,测试就是安全。是的,有时他们会因为测试错误而引入额外的工作。但在大多数情况下,它们可以节省大量时间。 (而且你有绝佳的机会惩罚打破测试的人(我们说的是橡皮鸡)。

        【讨论】:

          【解决方案10】:

          尽你所能测试。即使是微不足道的错误,例如忘记将米转换为英尺,也会产生非常昂贵的副作用。写一个测试,写代码让它检查,让它通过,继续前进。谁知道在未来的某个时候,有人可能会更改折扣代码。测试可以发现问题。

          【讨论】:

          • 这并没有解决我的任何想法。我了解 TDD 的基本原则......我没有看到好处。
          【解决方案11】:

          我认为单元测试和生产代码具有共生关系。简单地说:一个测试另一个。并且都测试开发者。

          【讨论】:

            【解决方案12】:

            请记住,随着缺陷在整个开发周期中持续存在,修复缺陷的成本会(以指数方式)增加。是的,测试团队可能会发现缺陷,但与单元测试失败相比,从该点隔离和修复缺陷(通常)需要更多的工作,并且如果您在修复它时引入其他缺陷会更容易没有要运行的单元测试。

            这通常比一个琐碎的例子更容易看到......而且对于琐碎的例子,如果你以某种方式弄乱了单元测试,审查它的人会发现测试中的错误或错误代码,或两者兼而有之。 (他们正在接受审查,对吗?)作为tvanfosson points out,单元测试只是 SQA 计划的一部分。

            从某种意义上说,单元测试是一种保险。他们不能保证你会发现每一个缺陷,有时你可能会在他们身上花费很多资源,但是当他们确实发现了你可以修复的缺陷时,你会花更少的钱与您根本没有测试并且必须修复下游的所有缺陷相比。

            【讨论】:

              【解决方案13】:

              我明白你的意思,但显然夸大了。

              您的论点基本上是:测试引入了失败。因此测试是不好的/浪费时间。

              虽然在某些情况下可能是这样,但几乎不是大多数情况。

              TDD 假设:更多测试 = 更少失败。

              测试更有可能捕捉失败点而不是引入失败点。

              【讨论】:

                【解决方案14】:

                更多的自动化可以在这里提供帮助! 是的,编写单元测试可能需要做很多工作,所以使用一些工具来帮助你。 如果您使用的是 .Net,请查看来自 Microsoft 的 Pex 之类的东西 它将通过检查您的代码自动为您创建单元测试套件。它会提供覆盖率良好的测试,尝试覆盖代码中的所有路径。

                当然,仅仅通过查看您的代码,它无法知道您实际尝试做什么,因此它不知道它是否正确。但是,它会为您生成有趣的测试用例,然后您可以检查它们,看看它的行为是否符合您的预期。

                如果你再进一步编写参数化单元测试(你可以将这些视为合同,真的),它会从这些生成特定的测试用例,这一次它可以知道是否有问题,因为你在测试中的断言会失败。

                【讨论】:

                  【解决方案15】:

                  我想了一些好办法来回答这个问题,并想与科学方法相提并论。 IMO,你可以改写这个问题,“你如何进行实验?”

                  实验验证了关于物理宇宙的经验假设(假设)。单元测试将测试关于它们调用的代码的状态或行为的假设。我们可以谈论一个实验的有效性,但那是因为我们知道,通过许多其他实验,有些东西不适合。它没有收敛有效性经验证据。我们不会设计一个新的实验来测试或验证一个实验的有效性,但我们可能会设计一个全新的实验

                  所以像实验一样,我们不会根据单元测试本身是否通过单元测试来描述单元测试的有效性。与其他单元测试一起,它描述了我们对其正在测试的系统所做的假设。此外,就像实验一样,我们尝试从我们正在测试的内容中消除尽可能多的复杂性。 “尽可能简单,但绝不简单。”

                  与实验不同,我们有一个诀窍来验证我们的测试是否有效,而不仅仅是收敛有效性。我们可以巧妙地引入一个我们知道应该被测试捕获的 bug,然后看看测试是否确实失败了。 (如果我们能在现实世界中做到这一点,我们对收敛有效性的依赖就会少得多!)更有效的方法是在实施之前观察你的测试失败(Red 中的红色步骤,绿色,重构)。

                  【讨论】:

                    【解决方案16】:

                    编写测试时需要使用正确的范例。

                    1. 首先编写测试。
                    2. 确保他们无法开始。
                    3. 让他们通过。
                    4. 在签入代码之前进行代码审查(确保对测试进行了审查。)

                    您不能总是确定,但它们改进了整体测试。

                    【讨论】:

                      【解决方案17】:

                      即使您不测试您的代码,您的用户也肯定会在生产环境中对其进行测试。用户在尝试使您的软件崩溃甚至发现非关键错误方面非常有创意。

                      修复生产中的错误比解决开发阶段的问题要昂贵得多。 作为副作用,您将因客户外流而失去收入。对于 1 个愤怒的客户,您可以指望 11 个失去或没有获得的客户。

                      【讨论】:

                        猜你喜欢
                        • 2012-01-08
                        • 2019-11-02
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2021-10-07
                        • 1970-01-01
                        • 1970-01-01
                        • 2010-10-04
                        相关资源
                        最近更新 更多