您如何对单元测试进行单元测试?[关闭]


89

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

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


5
在许多情况下,单元测试也是规范和文档!
史蒂文·劳

32
...然后进行单元测试单元测试的单元测试...但是单元^ 4测试和单元^ 5测试呢... aaaaaaaaaahhhhhhhhh!
dacracot

14
任何数量的测试都不会使您摆脱错误的规格。
比尔蜥蜴

4
还有其他人只是听到听起来像是在鼓掌吗?
gnovice

9
我认为合适的说法是“一直都是乌龟。”
奎因·泰勒,

Answers:


63

首先,测试就像安全性一样-您永远不能百分百确定自己已经掌握了安全性,但是每一层都增加了信心,并提供了一个框架,可以更轻松地解决仍然存在的问题。

其次,您可以将测试分为多个子例程,然后可以对其进行测试。当您有20个类似的测试时,创建一个(已测试的)子例程意味着您的主要测试是对该子例程的20个简单调用,这很可能是正确的。

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


2
为了扮演魔鬼的拥护者,我将更多的层次视为更多的失败点,但这并没有增加我的信心。在我的实际工作中,我与一个高度分布式的SOA企业中的许多团队一起工作。如果他们的层失败,那么每个团队都可能危害项目。
FlySwat

2
这就是为什么要使用模拟对象分别测试每个图层的原因。
Toon Krijthe

10
太好了,因此我的测试将使用IMockedComplexObject通过,但是当我在现实世界中实际使用ComplexObject时,它失败了……我什么都没得到。
FlySwat

16
@Jonathan-不,您已经确信自己的代码可以工作-假设您开发了ComplexObject的接口并已对该接口进行了充分的测试。最糟糕的是,您已经了解到您对ComplexObject的了解不是您所期望的。
tvanfosson

9
@FlySwat:响应您对IMockedComplexObject的评论,我引用Cwash对另一个答案的评论:“您不嘲笑您要测试的内容,您嘲笑您不想要测试的内容。”
Brian

39

测试错误不会破坏您的生产代码。至少,没有比没有测试更糟糕的了。因此,这不是一个“失败点”:测试不一定必须正确才能使产品真正起作用。他们可能必须是正确的,然后才能正常工作,但是修复任何损坏的测试的过程不会危害您的实现代码。

您可以将测试(甚至是像这样的琐碎测试)视为代码应该做什么的第二种意见。一种意见是测试,另一种意见是执行。如果他们不同意,那么您就知道自己有问题,您会近距离观察。

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

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

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

[关于编辑:我认为不正确的规范是不可避免的失败点。如果您不知道该应用程序应执行的操作,则很可能它不会执行该操作。但是编写反映规范的测试并不能放大这个问题,只是不能解决它。因此,您不必添加新的故障点,而只是在代码中表示现有的错误,而不是华夫饼文档。]


4
错误的测试将使破损的代码泛滥成灾。那就是引入故障的地方。它提供了一种错误的自信感。
FlySwat

9
没错,但是没有测试也可以破解掉代码。错误的想法是,如果代码通过了单元测试,那么它必须是正确的-我在职业生涯的早期就被治愈了。因此,坏了的单元测试不会让坏的代码泛滥成灾,而只会让它进入集成测试。
史蒂夫·杰索普

7
另外,即使错误的测试也可以捕获损坏的代码,只要它包含与实现不同的错误即可。这就是我的观点,即测试并不一定必须正确,而是可以将您吸引到您所关注的领域。
史蒂夫·杰索普

2
非常有趣的答案。
彼得

22

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

单元测试实际上并不能节省工作,它可以帮助您发现并防止错误。这是更多的工作,但这是正确的工作。它正在考虑以最低的粒度级别编写代码,并编写测试用例来证明它在给定的一组输入下可以在预期条件下运行。它隔离变量,因此您可以通过在出现错误时在正确的位置查看来节省时间。它节省了那组测试,以便您在必须进行后续更改时可以一次又一次地使用它们。

我个人认为,大多数方法并没有从包含TDD的货运软件工程中删除很多步骤,但是您不必遵守严格的TDD即可获得单元测试的好处。保留好零件,扔掉收益不大的零件。

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


“调用具有特定输入的方法,并将其与预期输出进行比较。” 但是,如果输出是复杂类型...如XML文档,该怎么办?您不能只是“ ==”,您必须编写特定的代码compare,然后您的compare方法可能有错误?
andy

@andy:您必须分别测试您的比较方法。对其进行全面测试后,就可以在其他测试中依靠它来工作了。
比尔蜥蜴2010年

很酷,谢谢比尔。我已经开始在一个新地方工作,这是我第一次参加单元测试。我认为原则上它是可行的,并且我们在真正有用的地方使用了Cruise Control,但是大量测试似乎和传统代码一样遭受命运……我只是不确定....
Andy

11

在那里进行单元测试,以便您的单元(方法)达到您的期望。首先编写测试会迫使您编写代码之前考虑一下您的期望。做事前思考总是一个好主意。

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

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

[编辑]

我在执行TDD时发现了另一件事。默认情况下,它几乎会强制执行良好的设计。这是因为高度耦合的设计几乎不可能单独进行单元测试。使用TDD并不需要很长时间就能弄清楚,使用接口,控制反转和依赖注入-所有可改进设计并减少耦合的模式-对于可测试代码而言确实很重要。


也许这就是我的问题所在。与可视化结果相比,我可以更轻松地查看业务规则的算法,因此实现代码本身没有问题,但是将规则模拟为多余的。也许就是我的想法。
FlySwat

这就是您在单元测试中所做的。将算法分解为多个部分,然后逐一检查。通常,我发现我的代码是自己编写的,因为我已经在单元测试中编写了期望值。
tvanfosson

模拟是测试空间中的一个超载术语。您不会嘲笑您要测试的内容,而是嘲笑您不想要测试的内容...当您为业务规则编写测试时,您会创建调用它的代码-根本没有在模拟它。
cwash

@cwash-我不确定您的评论如何适用于我的答案。我没有提到嘲讽……我也同意你的看法。
tvanfosson

@tvanfosson-我的最后评论是对@FlySwat的回应:“ ...将该规则嘲笑为多余。” 对不起,我忘了指定。
cwash

10

如何测试一个测试突变测试是我个人用来取得令人惊讶的良好效果的一项有价值的技术。阅读链接的文章以获取更多详细信息,以及更多学术参考的链接,但是通常,它通过修改源代码(例如将“ x + = 1”更改为“ x-= 1”)来“测试您的测试”,然后重新运行测试,确保至少一项测试失败。不会导致测试失败的任何突变都将标记为以后调查。

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


1
+1我还没有听说过的有趣概念
Wim Coenen,2009年

9

在应用测试驱动开发(TDD)时,首先要通过失败的测试。这似乎是不必要的步骤,实际上是在这里验证单元测试正在测试某些东西。的确,如果测试永不失败,它将毫无价值,而且更糟,因为您将依赖没有任何结果的积极结果,因此会导致错误的信心。

严格按照此过程进行操作时,所有“单元”都将受到单元测试所建立的安全网的保护,即使是最平凡的。

Assert.IsEqual(p.DiscountPrice,90);

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

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


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

测试失败时,红色表示JUnit红色栏。

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

在绿色条件下进行重构:删除所有重复项,提高可读性。


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

敏捷与直接沟通有关,而不是泛滥的需求文档。


8

虽然我全都在进行单元测试,但有时我想知道这种形式的测试优先开发是否真的有益……

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

例如,考虑进行一个琐碎的测试以探究如何使用您不熟悉的API。如果该测试与您使用“真实的” API的代码中的操作相关,那么保持该测试很有用。当API发布新版本时,您需要升级。现在,您对如何以一种可执行的格式记录该API的行为做出了假设,该格式可以用来捕获回归。

... [在实际过程中,您的代码(业务请求,需求文档,体系结构文档)上方有3-4层,其中实际定义的业务规则(折扣价为价格-折扣)可能会错误定义。如果是这种情况,您的单元测试对您没有任何意义。

如果您已经编写了多年代码,而没有编写测试,那么对于您来说可能没有立即意识到是否有任何价值。但是,如果您的心态是最好的工作方式是“过早发布,经常发布”或“敏捷”,因为您想要快速/连续部署的能力,那么您的测试肯定会有所帮助。做到这一点的唯一方法是通过测试使对代码所做的每项更改合法化。无论测试多么小,一旦有了绿色的测试套件,理论上就可以部署了。另请参见“连续生产”和“永久Beta版”。

您也不必“先测试”就可以拥有这种思维方式,但这通常是到达那里的最有效方法。当您执行TDD时,您将自己锁定在两到三分钟的红色绿色重构周期中。您绝对不会停下来离开,手上完全混乱,这将花费一个小时来调试并重新组装。

此外,单元测试是另一个失败点。

成功的测试是表明系统故障的测试。测试失败会警告您测试逻辑或系统逻辑中的错误。测试的目的是破坏代码或证明一种方案可行。

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

我们在这里能获得什么?

用这种方式做事绝对有好处。Michael Feathers将“旧版代码”定义为“未经测试的代码”。采用这种方法时,您会将对代码库进行的每项更改合法化。它比不使用测试更加严格,但是在维护大型代码库时,它是值得的。

说到“羽毛”,您应该参考以下两项重要资源:

这两篇文章都说明了如何将这些类型的实践和学科应用到非“绿地”项目中。它们提供了用于围绕紧密耦合的组件,硬连线的依赖项以及您不必控制的事物编写测试的技术。这全都在于寻找“接缝”并围绕这些接缝进行测试。

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

这些习惯就像一种投资。退货不是立即的;他们会随着时间的流逝而积累。不进行测试的替代方法实质上是承担着无法捕捉回归,无法担心集成错误而引入代码或驱动设计决策的负担。这样做的好处是您可以合法化代码库中引入的所有更改。

我在这里想念什么?请教我爱TDD,因为到目前为止我很难接受它。我也想要,因为我想保持进步,但这对我来说毫无意义。

我将其视为专业责任。努力奋斗是理想的。但是很难遵循且乏味。如果您关心它,并且感觉到您不应该生成未经测试的代码,那么您将能够找到学习良好测试习惯的意愿。我现在(和其他人一样)经常做的一件事是自己一个小时的时间来编写代码,根本不做任何测试,然后有纪律将其丢弃。这可能看起来很浪费,但实际上并非如此。锻炼并不会花费公司实际的材料。它帮助我理解了问题以及如何以更高的质量和可测试的方式编写代码。

我的建议最终是,如果您真的不希望擅长于此,那就根本不要这样做。未维护,性能不佳等不良测试可能比没有任何测试更糟糕。很难独自学习,并且您可能不会喜欢它,但是如果您不想这样做或无法看到足够的价值来学习,它将几乎是不可能的。保证时间投入。

一直有人提到测试有助于实施规范。根据我的经验,该规范也经常是错误的...

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

这里要考虑的另一件事是,过去已证明与测试基本相同的技术可以工作:“无尘室”和“按合同设计”都倾向于产生相同类型的“元”代码构造,测试可以执行,并在不同的地方执行。这些技术都不是灵丹妙药,严格的定价最终会使您付出代价,无法按时将产品推向市场。但这不是问题所在。这是关于保持所做的事情的能力。这对于大多数项目而言非常重要。


7

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

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

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

5

大多数单元测试,测试假设。在这种情况下,折扣价格应为价格减去折扣。如果您的假设是错误的,我敢打赌您的代码也是错误的。而且,如果您犯了一个愚蠢的错误,则测试将失败并予以纠正。

如果规则更改,则测试将失败,这是一件好事。因此,在这种情况下,您也必须更改测试。

通常,如果测试立即失败(并且您不使用测试优先设计),则测试或代码是错误的(或者,如果您的日子不好,则两者都会出错)。您可以使用常识(以及规范)来更正令人反感的代码并重新运行测试。

就像Jason所说的那样,测试就是安全。是的,有时由于错误的测试,他们会进行额外的工作。但是大多数情况下,它们可以节省大量时间。(而且您有绝佳的机会惩罚不及格的人(我们在谈论橡皮鸡))。


4

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


那没有解决我的任何想法。我了解TDD的基本原理...我看不到好处。
FlySwat

4

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


3

请记住,修复缺陷的成本会随着缺陷在整个开发周期中的增长而呈指数增长。是的,测试团队可能会发现缺陷,但是(通常)从这一点上隔离和修复缺陷要比单元测试失败要花费更多的工作,并且如果要修复的话,引入其他缺陷会更容易。没有要运行的单元测试。

通常,通过一个琐碎的示例而不是琐碎的示例,通常会更容易看到……好吧,如果您以某种方式弄乱了单元测试,那么查看它的人将抓住测试中的错误或代码中的错误,或者都。(正在对它们进行审查,对吗?)正如tvanfosson指出的那样,单元测试只是SQA计划的一部分。

从某种意义上说,单元测试是保险。它们不能保证您会捕获所有缺陷,有时似乎您在这些缺陷上花费了很多资源,但是当它们确实捕获了可以修复的缺陷时,您的支出将减少很多比起您根本没有进行测试,而必须修复下游所有缺陷而言。


3

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

您的观点基本上是:测试会导致失败。因此,测试是不好的/浪费时间。

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

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

测试比引入测试更容易抓住失败点。


1

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

当然,仅仅通过查看您的代码就无法知道您实际上在试图做什么,因此它也不知道它是否正确。但是,它将为您生成有趣的测试用例,然后您可以对其进行检查,以查看它是否按预期运行。

如果您走得更远并编写参数化的单元测试(实际上您可以将它们视为合同),它将从中生成特定的测试用例,这一次它可以知道是否出了问题,因为您在测试中的断言将失败。


1

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

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

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

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


1

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

  1. 首先编写测试。
  2. 确保他们不能以开始。
  3. 让他们通过。
  4. 签入代码之前,请先进行代码检查(确保已检查测试)。

您不能总是确定,但是它们可以改善整体测试。


0

即使您不测试代码,也一定会在您的用户中对其进行测试。用户在尝试使软件崩溃甚至发现非关键错误方面很有创造力。

与在开发阶段解决问题相比,修复生产中的错误的成本要高得多。副作用是,由于客户流失,您将损失收入。您可以指望11个失去或未获得的客户为1个生气的客户。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.