tdd - 如何对单元测试进行单元测试?

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

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

FlySwat asked 2019-08-24T20:48:53Z
17个解决方案
63 votes

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

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

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

Jason Cohen answered 2019-08-24T20:49:20Z
39 votes

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

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

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

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

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

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

Steve Jessop answered 2019-08-24T20:50:23Z
22 votes

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

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

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

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

Bill the Lizard answered 2019-08-24T20:51:07Z
11 votes

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

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

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

[编辑]

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

tvanfosson answered 2019-08-24T20:51:56Z
10 votes

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

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

Andrzej Doyle answered 2019-08-24T20:52:29Z
9 votes

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

严格遵循此流程,所有'''''' 受到单元测试所产生的安全网的保护,即使是最普通的。

Assert.IsEqual(p.DiscountPrice,90);

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

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


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

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

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

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


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

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

philant answered 2019-08-24T20:53:58Z
8 votes

虽然,我全都是单元测试,我   有时候想知道这种形式的测试   第一次开发真的很有益......

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

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

...... [我]一个真正的过程,你有3-4   代码上方的图层(业务   请求,要求文件,   建筑文件),在哪里   实际定义的业务规则(折扣   价格是价格 - 折扣)可能  misdefined。 如果那样的话,   你的单元测试对你没什么意义。

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

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

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

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

如果您在代码之后编写测试,那么您将面临编写“不良”测试的风险。 因为为了看到你的测试确实有效,你需要看到它既破碎又有效。 当你在代码之后编写测试时,这意味着你必须弹出陷阱" 并在代码中引入一个错误,以查看测试失败。 大多数开发人员不仅对此感到不安,而且认为这是浪费时间。

我们在这里获得了什么?

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

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

  • 有效地使用遗留代码
  • .NET中的Brownfield应用程序开发

这两个都解释了如何将这些类型的实践和学科应用到不是格林菲尔德的项目中。" 它们提供了围绕紧密耦合的组件编写测试的技术,硬连线依赖项以及您不必控制的事物。 这完全取决于找到"接缝" 并围绕这些进行测试。

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

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

我在这里错过了什么? 请教   我喜欢TDD,因为我很努力   到目前为止,接受它是有用的。 一世   我也想要,因为我想留下来   进步,但它只是没有   对我有意义。

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

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

有几个人一直提到这一点   测试有助于执行规范。 它有   是我的经验,规范有   也是错的,经常是错的   不...

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

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

cwash answered 2019-08-24T20:57:02Z
7 votes

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

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

  • 它可以帮助您找到一个很好的解耦设计。 您只能对经过良好解耦的代码进行单元测试;
  • 它可以帮助您确定何时完成。 必须在测试中指定所需的行为有助于不构建您实际不需要的功能,并确定功能何时完成;
  • 它为重构提供了一个安全网,使代码更容易适应变化; 和
  • 它节省了大量的调试时间,这是非常昂贵的(我听说传统上,开发人员花费高达80%的时间调试)。
Ilja Preuß answered 2019-08-24T20:58:02Z
5 votes

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

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

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

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

Toon Krijthe answered 2019-08-24T20:58:48Z
4 votes

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

Jim C answered 2019-08-24T20:59:12Z
4 votes

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

Johnsyweb answered 2019-08-24T20:59:37Z
3 votes

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

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

从某种意义上说,单元测试是保险。 他们并不能保证你能抓住每一个缺陷,有时你可能会花费大量的资源来解决这些缺陷,但是当他们确实发现了你可以解决的缺陷时,你就会#&# 如果你没有完全没有测试并且不得不修复下游的所有缺陷,那么花费会少得多。

Dave DuPlantis answered 2019-08-24T21:00:17Z
3 votes

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

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

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

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

测试比引入测试更容易发现故障点。

Ray answered 2019-08-24T21:01:09Z
1 votes

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

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

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

answered 2019-08-24T21:01:48Z
1 votes

我已经想过一个回答这个问题的好方法,并希望与科学方法并行。 国际海事组织,你可以改写这个问题,"你如何试验一个实验?"

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

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

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

cwash answered 2019-08-24T21:02:35Z
1 votes

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

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

你总是不确定,但他们改善了整体测试。

Jonathan answered 2019-08-24T21:03:34Z
0 votes

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

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

answered 2019-08-24T21:04:06Z
translate from https://stackoverflow.com:/questions/244345/how-do-you-unit-test-a-unit-test