你如何对单元测试进行单元测试?

时间:2008-10-28 18:37:06

标签: tdd agile test-first

我在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,因为到目前为止我很难接受TDD。我也想要,因为我想保持进步,但这对我来说没有意义。

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

17 个答案:

答案 0 :(得分:63)

首先,测试就像安全一样 - 你永远无法100%确定你已经得到它,但每一层都增加了信心和框架,可以更轻松地解决剩下的问题。

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

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

答案 1 :(得分:39)

错误的测试不太可能破坏您的生产代码。至少,没有比没有测试更糟糕的了。因此,它不是“失败点”:为了使产品真正起作用,测试不必是正确的。它们在签署工作之前可能必须是正确的,但修复任何损坏的测试的过程不会危及您的实现代码。

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

如果将来某人想要从头开始实现相同的界面,这也很有用。他们不应该阅读第一个实现,以了解Discount的含义,并且测试可以作为对您可能具有的任何接口的书面描述的明确支持。

那就是说,你正在折腾时间。如果还有其他测试,您可以使用保存跳过这些琐碎测试的时间来编写,也许它们会更有价值。这取决于您的测试设置和应用程序的性质。如果折扣对应用很重要,那么无论如何你都会在功能测试中捕获这种方法中的任何错误。所有单元测试都可以让您在测试此单元的时候捕获它们,当错误的位置立即显而易见时,而不是等到应用程序集成在一起并且错误的位置可能< / em>不太明显。

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

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

答案 2 :(得分:22)

  
    

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

  

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

我个人认为大多数方法并没有从cargo cult software engineering中删除很多步骤,包括TDD,但是您不必遵守严格的TDD来获得单元测试的好处。保留良好的部件,扔掉几乎没有收益的部件。

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

答案 3 :(得分:11)

单元测试在那里,以便你的单位(方法)做你期望的。首先编写测试会强制您考虑在编写代码之前之前的内容。做之前的思考总是一个好主意。

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

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

[编辑]

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

答案 4 :(得分:10)

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

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

答案 5 :(得分:9)

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

严格遵循此流程时,所有“单位”都受到单位测试所带来的安全网的保护,即使是最普通的。

Assert.IsEqual(p.DiscountPrice,90);

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

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


红色 - 绿色 - 重构 - 这是为了记住TDD过程的本质。

当测试失败时,

红色指的是JUnit红色条。

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

绿色条件下的

重构:删除任何重复,提高可读性。


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

敏捷是关于直接沟通而不是抛弃墙上的要求文件。

答案 6 :(得分:8)

答案 7 :(得分:7)

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

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

  • 它可以帮助您找到一个很好的解耦设计。您只能单元测试分离良好的代码;
  • 它可以帮助您确定何时完成。必须在测试中指定所需的行为有助于不构建您实际不需要的功能,并确定功能何时完成;
  • 它为重构提供了一个安全网,使代码更易于修改;和
  • 它为您节省了大量的调试时间,这是非常昂贵的(我听说传统上,开发人员花费高达80%的时间调试)。

答案 8 :(得分:5)

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

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

作为一般规则,如果测试立即失败(并且您不使用测试优先设计),则测试或代码是错误的(或者如果您有糟糕的一天,则两者都是错误的)。您可以使用常识(并且可以通过规范)来纠正有问题的代码并重新运行测试。

像杰森说的那样,测试就是安全性。是的,有时他们会因为测试错误而引入额外的工作。但大部分时间他们都是节省大量时间的人。 (而且你有机会惩罚那个打破考试的人(我们正在谈论橡皮鸡))。

答案 9 :(得分:4)

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

答案 10 :(得分:4)

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

答案 11 :(得分:3)

请记住,由于缺陷存在于开发周期中,因此修复缺陷的成本会(指数级地)增加。是的,测试团队可能会发现缺陷,但是(通常)需要做更多工作来隔离和修复缺陷,而不是单元测试失败,并且如果你修复它会更容易引入其他缺陷没有单元测试运行。

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

从某种意义上说,单元测试是保险。他们不能保证你会抓住每一个缺陷,有时你可能会花费大量的资源,但是当他们确实发现你可以解决的缺陷时,你会花很多钱而不是你根本没有测试,并且必须解决下游的所有缺陷。

答案 12 :(得分:3)

我明白你的观点,但显然被夸大了。

你的论点基本上是:测试引入失败。因此,测试很糟糕/浪费时间。

虽然在某些情况下这可能是正确的,但它并不是多数。

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

测试更可能来捕捉失败点而不是介绍它们。

答案 13 :(得分:1)

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

当然,只是通过查看你的代码,它无法知道你实际上在做什么,所以它不知道它是否正确。但是,它会为您生成有趣的测试用例,然后您可以检查它们并查看它是否按预期运行。

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

答案 14 :(得分:1)

我想到了一个回答这个问题的好方法,并想与科学方法并行。国际海事组织,你可以改写这个问题,“你如何试验实验?”

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

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

与实验不同,我们有一个技巧来验证我们的测试是否有效,而不仅仅是收敛有效性。我们可以巧妙地介绍一个我们知道应该被测试捕获的错误,并查看测试是否确实失败了。 (如果我们只能在现实世界中做到这一点,我们更少依赖于这种收敛有效性的东西!)更有效的方法是在实现它之前观察你的测试失败( Red中的红色步骤,绿色,重构)。

答案 15 :(得分:1)

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

  1. 首先编写测试。
  2. 确保他们无法开始。
  3. 让他们通过。
  4. 在检查代码之前进行代码审查(确保审核测试。)
  5. 你总是不确定,但他们会改善整体测试。

答案 16 :(得分:0)

即使您不测试代码,您的用户也一定会对其进行测试。用户在尝试使软件崩溃并发现非严重错误时非常有创意。

修复生产中的错误比解决开发阶段的问题要昂贵得多。 作为一种副作用,由于顾客的流失,你将失去收入。对于一个愤怒的客户,您可以依靠11个丢失或未获得的客户。