没有TDD的单元测试感


28

我们有一个新的(相当大的)项目正在启动,我们计划使用TDD进行开发。

TDD的想法失败了(有很多业务和非业务原因),但是现在我们正在进行对话-是否应该编写单元测试。我的朋友说,在没有TDD的情况下编写单元测试没有任何意义(或几乎为零),我们应该只关注集成测试。我相信相反,编写简单的单元测试仍然有一定的道理,只是为了使代码更可靠。你怎么看?

补充:我认为这不是>> thisquest <<的重复-我了解UT和TDD之间的区别。我的问题不是关于差异,而是关于编写没有TDD的单元测试的感觉


22
我很好奇您的朋友对这种荒谬的立场有什么理由……
Telastyn 2014年

11
我敢打赌,绝大多数带有某些单元测试的项目都没有使用TDD。
Casey

2
您的集成水平是多少?您的单位是什么?在每个测试级别以下,您将重构的频率是多少?您的集成测试将运行多快?他们写的容易程度如何?您的代码的不同部分会生成多少组合用例?等等...如果您不知道这些答案,那么现在做出坚定的决定还为时过早。有时TDD很棒。有时,收益不清楚。有时单元测试是必不可少的。有时,一套不错的集成测试可以为您带来几乎同等的购买力,并且更加灵活。
topo恢复莫妮卡

2
作为经验的一些实用建议,如果您不进行TDD ,请不要对所有内容进行测试。确定哪些测试有价值。我发现纯输入/输出方法的单元测试非常有价值,而在应用程序的很高级别(例如,实际上在Web应用程序上发送Web请求)的集成测试也非常有价值。注意需要大量模拟设置的中间层集成测试和单元测试。另请观看此视频:youtube.com/watch?v=R9FOchgTtLM
jpmc26 2014年

对于您提出的问题,您的更新没有任何意义。如果您了解TDD和单元测试之间的区别,那么您将无法编写单元测试。投票让您的问题保持关闭,尽管我可以看到关闭的论点是“不清楚您要问什么”,而不是重复。

Answers:


52

TDD主要用于(1)确保覆盖范围(2)并推动可维护,可理解,可测试的设计。如果您不使用TDD,则无法保证代码覆盖率。但这绝不意味着您应该放弃该目标,并以0%的覆盖率过活。

发明回归测试是有原因的。原因是,从长远来看,相比于花费更多的精力编写,它们可以为您节省更多的时间以防止出错。一遍又一遍地证明了这一点。因此,除非你是认真相信,你的组织是好得多在软件工程比所有谁推荐的回归测试大师(或者,如果你打算下去很快,以便有没有长期来看对你),是的,你绝对应该进行单元测试,这完全适用于世界上几乎所有其他组织的原因:因为它们比集成测试更早地发现错误,并且可以为您省钱。不写它们就像在街上闲散闲钱。


12
“如果不使用TDD,就无法保证代码覆盖率。”:我不这么认为。您可以开发两天,然后在接下来的两天内编写测试。重要的一点是,除非您具有所需的代码覆盖率,否则您不会认为功能已完成。
乔治

5
@DougM-也许在理想世界中……
Telastyn 2014年

7
可悲的是TDD去手牵手与嘲讽,而且所有这证明是你的测试运行,直到人停止做较快TDD已死。万岁测试。
MickyD 2014年

17
TDD 不保证代码覆盖率。 这是一个危险的假设。您可以针对测试编写代码,通过这些测试,但是仍然有一些极端情况。
罗伯特·哈维

4
@MickyDuncan我不太确定我是否完全理解您的关注。模拟是一种非常有效的技术,用于将一个组件与另一个组件隔离开来,以便可以独立执行该组件行为的测试。是的,极端使用会导致软件过度设计,但是如果使用不当,任何开发技术也可能会导致过度设计。此外,正如DHH在您引用的文章中所指出的那样,仅使用完整的系统测试的想法同样糟糕,即使实际上并不糟糕。 重要的是要使用判断力来决定测试任何特定功能的最佳方法是什么
2014年

21

对于我现在发生的事情,我有一个相关的轶事。我在一个不使用TDD的项目中。我们的质量检查人员正在朝着这个方向发展,但我们的组织规模很小,这是一个漫长而漫长的过程。

无论如何,我最近正在使用第三方库来执行特定任务。关于该库的使用存在问题,因此我不得不自己编写该库的一个版本。总的来说,它最终是大约5,000行可执行代码,大约花了我2个月的时间。我知道代码行是一个很差的指标,但是对于这个答案,我觉得它是一个不错的指标。

我需要一种特殊的数据结构,该结构可使我跟踪任意数量的位。由于该项目是Java语言,因此我选择了Java语言BitSet并对其进行了一些修改(我还需要具有跟踪前导0s 的能力,而Java的BitSet由于某种原因而无法使用.....)。在达到约93%的覆盖率之后,我开始编写一些实际上会对我编写的系统产生压力的测试。我需要对功能的某些方面进行基准测试,以确保它们可以满足我的最终需求。毫不奇怪,BitSet在处理大位集(在这种情况下为数亿个位)时,我从接口覆盖的功能之一非常慢。其他重写功能依赖于此功能,因此这是一个巨大的瓶颈。

我最终要做的是转到绘图板,并找出一种方法来操纵的底层结构BitSet,即long[]。我设计了算法,请同事提供输入,然后着手编写代码。然后,我运行了单元测试。他们中的一些人破产了,确实有人指出我需要解决算法中的问题。修复了单元测试中的所有错误之后,我可以说该功能可以正常工作。至少,我可以确信这种新算法与以前的算法一样有效。

当然,这不是防弹的。如果我的代码中有一个错误,单元测试没有检查,那么我就不会知道。但是,当然,我的慢速算法中也可能存在相同的错误。 但是,我可以很有把握地说,我不必担心该特定函数的错误输出。预先存在的单元测试为我节省了数小时甚至数天的时间来尝试测试新算法,以确保它是正确的。

就是不管TDD都进行单元测试的意义所在-也就是说,当最终重构/维护代码时,单元测试将在TDD中和TDD外部为您做到这一点。当然,这应该与常规回归测试,冒烟测试,模糊测试等配合使用,但是正如名称所指出的那样,单元测试可以在最小的原子级别上测试事物,从而为您指出发生错误的位置。

就我而言,如果没有现有的单元测试,我将不得不提出一种确保算法始终有效的方法。最后,这听起来很像单元测试,不是吗?


7

您可以将代码大致分为4类:

  1. 简单且很少更改。
  2. 简单而频繁的更改。
  3. 复杂且很少变化。
  4. 复杂且频繁变化。

单元测试在您选择的列表中越靠后,就越有价值(可能会捕获重要的错误)。在我的个人项目中,我几乎总是在类别4上进行TDD。在类别3上,我通常会进行TDD,除非手动测试更简单,更快捷。例如,抗锯齿代码编写起来会很复杂,但是比编写单元测试要容易地进行视觉验证,因此只有在代码频繁更改的情况下,单元测试才对我有价值。我剩下的代码只有在发现该函数中的错误之后才进行单元测试。

有时很难事先知道某个代码块适合的类别。TDD的价值在于您不会偶然错过任何复杂的单元测试。TDD的成本是您花费所有时间编写简单的单元测试。但是,通常有项目经验的人会以一定程度的确定性知道代码不同部分适合的类别。如果您不进行TDD测试,则至少应尝试编写最有价值的测试。


1
当像您在抗锯齿示例中建议的那样处理代码时,我发现最好的方法是实验性地开发代码,然后添加一些特性测试以确保以后不会意外破坏算法。表征测试非常快速且易于开发,因此这样做的开销非常低。
2014年

1

无论是单元测试,组件测试,集成测试还是验收测试,重要的是必须将其自动化。从简单的CRUD到最复杂的计算,任何类型的软件都没有自动测试的致命错误。原因是编写自动化测试的成本总是比不进行手动运行所有测试的持续需求要少几个数量级。编写完它们之后,只需按一下按钮即可查看它们是否通过或失败。手动测试总是需要很长时间才能运行,并且取决于人类(无聊的生物,可能缺乏注意力等)来检查测试是否通过。简而言之,请始终编​​写自动化测试。

现在,关于您的同事可能反对在没有TDD的情况下进行任何类型的单元测试的原因:可能是因为更难以信任在生产代码之后编写的测试。如果你不能信任你的自动化测试,他们是值得什么。在TDD周期之后,必须首先使测试失败(出于正确的原因),然后才能编写生产代码以使其通过(再没有更多)。这个过程实质上是测试您的测试,因此您可以信任它们。更不用说在实际代码之前编写测试的事实,这促使您将单元和组件设计为更易于测试(去耦程度高,应用了SRP等)。虽然,当然,进行TDD需要纪律处分

相反,如果您首先编写所有生产代码,则在为它编写测试时,您会期望它们在首次运行时通过。这是非常有问题的,因为您可能创建了覆盖100%生产代码的测试,而没有断言正确的行为(甚至可能没有执行任何断言!我已经看到了这种情况),因为您看不到它会失败首先检查是否由于正确的原因而失败。因此,您可能会有假阳性。错误肯定会最终破坏您对测试套件的信任,从本质上迫使人们再次诉诸于手动测试,因此您要花掉这两个过程的成本(编写测试+手动测试)。

这意味着您必须像TDD一样找到另一种测试测试的方法。因此,您可以调试,注释生产代码的一部分等,以便能够信任测试。问题是这种方式“测试您的测试”的过程要慢得多。根据我的经验,将此时间加到您将要花费的时间来手动运行临时测试(因为在编写生产代码时没有自动测试)会导致整个过程比练习慢得多TDD“按书”(肯特·贝克-TDD示例)。另外,我愿意在这里打赌,说在编写测试之后真正地“测试您的测试” 比TDD 需要更多的纪律

因此,也许您的团队可以因为没有进行TDD而重新考虑“业务和非业务原因”。根据我的经验,人们倾向于认为与仅在代码完成后编写单元测试相比,TDD速度较慢。正如您在上面已经读过的那样,这种假设是有缺陷的。


0

通常,测试的质量取决于其出处。我经常犯不执行“真实” TDD的罪行-我编写了一些代码来证明我想使用的策略确实有效,然后讲解每种代码打算在以后进行测试的情况。通常,代码单元是一个类,目的是让您大致了解在没有测试覆盖率的情况下我会幸福地完成多少工作-也就是说,不是很多。这意味着测试的语义含义与处于“完成”状态的被测系统很好地匹配-因为我是在知道SUT满足什么情况以及如何实现的情况下编写它们的。

相反,TDD采取积极重构的策略往往会使测试过时的速度至少与将其编写为要更改的系统的公共接口一样快。我个人认为,两种设计应用程序的功能单元的脑力负荷,并保持覆盖它同步太高,以保持我的浓度和测试维护频频滑倒测试的语义。代码库最终会产生无法测试任何有价值的测试或只是纯属错误的测试。如果您有纪律和精神上的能力来使测试套件保持最新,则一定要严格按照自己的意愿练习TDD。我没有,因此我发现它不太成功。


0

实际上,鲍伯叔叔在他的Clean Coders视频中提到了一个非常有趣的观点。他说,红色-绿色重构循环可以以两种方式应用。

第一种是传统的TDD方式。编写失败的测试,然后使测试通过并最终进行重构。

第二种方法是编写非常小的生产代码,并立即对其进行单元测试,然后进行重构。

这个想法是非常小的步骤。当然,您会从生产代码中丢失测试从红色变为绿色的验证,但是在某些情况下,我主要与初级开发人员合作,他们甚至拒绝尝试理解TDD,但事实证明这种方法有些有效。

我再次重复一次(鲍勃叔叔强调了这一点),其想法是非常小的步骤,并立即测试刚刚添加的生产代码。


“ ...的想法是走很小的一步,立即测试刚刚添加的生产代码。”:我不同意。当您已经清楚要做什么并且想在细节上工作时,您所描述的如果很好,但是您需要首先了解大局。否则,仅需进行很小的步骤(测试,开发,测试,开发),您就会迷失在细节中。“如果你不知道要去哪里,那你可能不会到达那里。”
Giorgio 2014年
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.