当前位置:  开发笔记 > 后端 > 正文

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

如何解决《你如何对单元测试进行单元测试?》经验,为你挑选了9个好方法。

我在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.我也想要,因为我想保持进步,但这对我来说没有意义.

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



1> Jason Cohen..:

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

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

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


@Jonathan - 不,你已经对你的代码工作充满信心 - 假设你开发了ComplexObject的接口并对该接口进行了充分的测试.在最坏的情况下,您已经了解到您对ComplexObject的理解不是您所预期的.
太好了,所以我的测试将使用IMockedComplexObject传递但是当我在现实世界中实际使用ComplexObject时,它失败了......我再也没有获得任何东西了.
@FlySwat:在回应你对IMockedComplexObject的评论时,我引用了Cwash对另一个答案的评论:"你不会嘲笑你要测试的内容,你嘲笑你不想测试的内容."
为了扮演恶魔的拥护者,我认为更多可能的失败点是附加层,这不会增加我的信心.在我的实际工作中,我与许多团队一起工作,而不是高度分布的SOA企业.如果他们的图层失败,那么每个团队都可能危及项目.
这就是您使用模拟对象分别测试每个图层的原因.
@Jonathan - 如果你编写了一个使用IMockedComplexObject的测试,它不是你正在测试的ComplexObject,它是一个正在测试的ComplexObject的客户端,我们不想要实例化ComplexObject.之后,您仍需要在单独的测试中测试ComplexObject.

2> Steve Jessop..:

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

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

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

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

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

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


这是真的,但没有测试也会让代码破碎.错误是认为如果代码通过单元测试它必须是正确的 - 我在职业生涯的早期就已经治好了.因此,破坏的单元测试不会让破解的代码变得疯狂,它只会让它进入集成测试.
此外,即使错误的测试也可以捕获破坏的代码,只要它包含来自实现的不同错误.我的观点是,测试并非绝对必须是正确的,他们会把你的注意力吸引到关注的领域.
错误的测试会让破解的代码变得疯狂.那就是失败的地方.它提供了一种虚假的自信心.
非常有趣的答案.

3> Bill the Liz..:

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

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

我个人认为大多数方法并没有从货物崇拜软件工程中删除很多步骤,包括TDD,但是您不必遵守严格的TDD来获得单元测试的好处.保留好零件并丢弃几乎没有效益的零件.

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



4> tvanfosson..:

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

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

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

[编辑]

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



5> Andrzej Doyl..:

如何测试测试? 变异测试是一种有价值的技术,我个人习惯使用效果出乎意料的好.阅读链接文章了解更多详细信息,并链接到更多学术参考文献,但一般来说,它通过修改源代码"测试您的测试"(例如将"x + = 1"更改为"x - = 1")然后重新运行测试,确保至少一个测试失败.任何不会导致测试失败的突变都会被标记以供以后调查.

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



6> philant..:

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

严格遵循这一过程,所有"单位"都受到单位测试所带来的安全网的保护,即使是最平凡的.

Assert.IsEqual(p.DiscountPrice,90);

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

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


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

当测试失败时,红色表示JUnit红色条.

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

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


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

敏捷是关于直接沟通而不是抛出墙上的需求文档.



7> cwash..:

虽然,我是全部用于单元测试,但我有时想知道这种形式的测试首次开发是否真的有益......

像这样的小型,微不足道的测试可以为您的代码库中的"煤矿中的金丝雀",在为时已晚之前警告危险.琐碎的测试对于保持有用是有用的,因为它们可以帮助您正确地进行交互.

例如,考虑一个简单的测试,以探讨如何使用您不熟悉的API.如果该测试与您在使用API​​"for real"的代码中所做的事情有任何关联,那么保持该测试是有用的.当API发布新版本并且您需要升级时.现在,您可以假设您希望API以可执行格式记录,以便捕获回归.

... [我]实际流程,您的代码(业务请求,需求文档,架构文档)上方有3-4层,其中实际定义的业务规则(折扣价格是价格 - 折扣)可能会被错误定义.如果是这种情况,您的单元测试对您来说毫无意义.

如果您在没有编写测试的情况下编写了多年的编码,那么您可能不会立即明白它有任何价值.但是,如果您认为最好的工作方式是"尽早发布,经常发布"或"敏捷",因为您希望能够快速/持续地部署,那么您的测试肯定意味着什么.实现此目的的唯一方法是通过测试使您对代码所做的每一项更改合法化.无论测试多么小,一旦你有一个绿色测试套件,理论上你可以部署.另请参阅"连续生产"和"永久测试版".

你也不必一直"先测试"成为这种心态,但这通常是达到目标的最有效方式.当您进行TDD时,您将自己锁定为两到三分钟的红绿重构周期.在任何时候你都无法停下来离开并且手上有一个完整的混乱,需要一个小时来调试并重新组合在一起.

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

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

如果您在代码之后编写测试,则可能会编写一个"糟糕"的测试,因为为了确保您的测试真正起作用,您需要看到它既破坏又有效.当您在代码之后编写测试时,这意味着您必须"弹出陷阱"并在代码中引入错误以查看测试失败.大多数开发人员不仅对此感到不安,而且认为这是浪费时间.

我们在这里获得了什么?

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

说到Feathers,有两个很好的资源你应该检查一下:

有效地使用遗留代码

.NET中的Brownfield应用程序开发

这两个都解释了如何将这些类型的实践和学科应用到不是"绿地"的项目中.它们提供了围绕紧密耦合的组件,硬连线依赖项以及您不一定能控制的事情编写测试的技术.这一切都是为了寻找"接缝"并围绕这些进行测试.

[我]折扣价格错误,测试团队仍然会发现问题,单元测试如何保存任何工作?

这些习惯就像投资一样.退货不是即时的; 他们随着时间的推移积累起 不进行测试的替代方案主要是承担无法获得回归的债务,引入代码而不必担心集成错误,或推动设计决策.美妙之处在于您将引入代码库的每个变更合法化.

我在这里错过了什么?请教我爱TDD,因为到目前为止我很难接受TDD.我也想要,因为我想保持进步,但这对我来说没有意义.

我认为这是一项职业责任.这是一个努力的理想.但是很难追随和乏味.如果您关心它,并且觉得您不应该生成未经过测试的代码,那么您将能够找到学习良好测试习惯的意志力.我现在做的很多事情(就像其他人一样)是时间盒我自己花了一个小时编写代码而没有任何测试,然后有纪律把它扔掉.这可能看起来很浪费,但事实并非如此.这不是运动花费公司的物质材料.它帮助我理解了这个问题以及如何编写代码,使其具有更高的质量和可测试性.

我的建议最终是,如果你真的不想要擅长它,那么根本不要这样做.不能保持不良,性能不佳等不良测试可能比没有进行任何测试更糟糕.你自己很难学习,而且你可能不会喜欢它,但是如果你没有这样的想法,或者看不到足够的价值,它几乎是不可能的.保证投入时间.

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

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

在这里要考虑的另一件事是,与测试基本相同的技术已被证明在过去有效:"洁净室"和"按合同设计"都倾向于生成相同类型的"元"代码构造,测试,并在不同点执行.这些技术都不是银子弹,而且在产品上市时间方面的严谨性将使您最终无法满足.但这不是它的意义所在.这是关于能够维持你提供的东西.这对大多数项目来说非常重要.



8> Ilja Preuß..:

单元测试与双重记录保存非常相似.您以两种截然不同的方式陈述相同的事物(业务规则)(如生产代码中的编程规则,以及测试中的简单,有代表性的示例).你在两者中犯同样的错误是不太可能的,所以如果他们彼此都认同,你就不太可能弄错了.

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

它可以帮助您找到一个很好的解耦设计.您只能对经过良好解耦的代码进行单元测试;

它可以帮助您确定何时完成.必须在测试中指定所需的行为有助于不构建您实际不需要的功能,并确定功能何时完成;

它为重构提供了一个安全网,使代码更容易适应变化; 和

它节省了大量的调试时间,这是非常昂贵的(我听说估计传统上,开发人员花费高达80%的时间调试).



9> Toon Krijthe..:

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

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

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

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

推荐阅读
黄晓敏3023
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有