我正在与一位同事讨论单元/集成测试,他提出了一个有趣的案例来反对编写单元测试。我是大型单元测试(主要是JUnit)的支持者,但是我很想听听其他人的看法,因为他提出了一些有趣的观点。
总结他的观点:
- 当发生重大代码更改时(新的POJO集,主要应用程序重构等),单元测试往往被注释掉而不是重新编写。
- 最好将时间花在涉及用例的集成测试上,这会使范围较小的测试变得不那么重要。
有这个想法吗?尽管集成测试听起来至少有价值,但我仍在进行单元测试(因为我一直认为它会产生改进的代码)。
我正在与一位同事讨论单元/集成测试,他提出了一个有趣的案例来反对编写单元测试。我是大型单元测试(主要是JUnit)的支持者,但是我很想听听其他人的看法,因为他提出了一些有趣的观点。
总结他的观点:
有这个想法吗?尽管集成测试听起来至少有价值,但我仍在进行单元测试(因为我一直认为它会产生改进的代码)。
Answers:
我倾向于支持您的朋友,因为单元测试经常会测试错误的东西。
单元测试并非天生就不好。但是他们经常测试实现细节而不是输入/输出流程。当发生这种情况时,您最终将获得完全毫无意义的测试。我自己的规则是,好的单元测试会告诉您您刚破坏了某些内容;不良的单元测试只会告诉您您刚刚更改了某些内容。
我脑海中的一个例子是几年前被纳入WordPress的一项测试。被测试的功能围绕相互调用的过滤器进行,测试正在验证是否可以正确的顺序调用回调。但是测试不是(a)运行链以验证是否按预期顺序调用了回调,而是将测试重点放在(b)读取可能不应该公开的某些内部状态上。更改内部零件,并且(b)变为红色;而(a)仅在内部结构的更改超出预期结果时才变为红色。(b)在我看来显然是毫无意义的考验。
如果您有一个向外界公开一些方法的类,那么在我看来,要测试的正确方法仅是后一种方法。如果还测试内部逻辑,则最终可能会使用复杂的测试方法或一连串的单元测试将内部逻辑暴露给外界,而这些单元测试总是会在您要更改任何内容时中断。
话虽如此,如果您的朋友对您所建议的单元测试本身如此严格,我会感到惊讶。相反,我会认为他很务实。也就是说,他观察到实际上编写的单元测试几乎没有意义。引用:“单元测试往往被注释掉而不是重新编写”。对我来说,那里隐含着一个信息-如果他们倾向于重新加工,那是因为他们倾向于吮吸。假设是这样,第二个命题是:开发人员将花费更少的时间来编写更难出错的代码-即集成测试。
因此,这不是一个好坏。只是容易出错,实际上在实践中经常会出错。
当发生重大代码更改时,单元测试倾向于被注释掉而不是重新编写。
一群杂乱无章的牛仔编码员认为,当您注释掉所有现有测试时,所有测试都变成“绿色”就可以满足了:当然。返工单元测试需要与第一手测试相同的纪律,所以您要做的是团队的“完成定义”。
最好将时间花费在涵盖用例的集成测试上,这会使范围较小的测试变得不那么重要。
这既不是完全错误也不是完全正确。编写有用的集成测试的工作很大程度上取决于您正在编写哪种软件。如果您拥有一个可以几乎像编写单元测试一样容易编写集成测试的软件,它们仍然运行很快,并且可以很好地覆盖您的代码,那么您的同事很有意义。但是对于许多系统,我已经看到集成测试无法轻松实现这三点,这就是为什么您应该努力进行单元测试。
我不知道我接受你同事的论点。
还有其他一些更令人信服的反对单元测试的理由。好吧,不完全反对他们。从DHH的测试引起的设计损害开始。他是一个有趣的作家,所以一定要读。我从中得到的是:
如果进行重大设计更改以适应单元测试,则它们可能会阻碍项目中的其他目标。如果可能,请问一个公正的人哪种方法更容易遵循。如果您难以理解的可高度测试的代码,则可能会失去从单元测试中获得的所有好处。过多抽象的认知开销会使您减速,并使泄漏错误变得更容易。不要打折。
如果您想获得细微差别和哲学性,这里有很多很棒的资源:
当发生重大代码更改时(新的POJO集,主要应用程序重构等),单元测试往往被注释掉而不是重新编写。
不要那样做 如果您找到这样做的人,请杀死他们并确保他们不会繁殖,因为他们的孩子可能会继承该有缺陷的基因。我从观看CSI电视节目中看到的关键是,在各处散布许多误导性的DNA样本。这样,您会以极大的噪音污染犯罪现场,鉴于DNA测试昂贵且耗时的特性,他们将其固定在您身上的可能性极低。
当发生重大代码更改时(新的POJO集,主要应用程序重构等),单元测试往往被注释掉而不是重新编写。
我总是尝试将重构和功能更改分开。当我需要同时执行这两项操作时,通常会先提交重构。
当重构代码而不更改功能时,现有的单元测试应该有助于确保重构不会意外破坏功能。因此,对于这样的提交,我将考虑禁用或删除单元测试是主要的警告信号。在检查代码时,应告知任何这样做的开发人员不要这样做。
不更改功能的更改仍可能由于有缺陷的单元测试而导致单元测试失败。如果您了解要更改的代码,则通常会立即发现此类单元测试失败的原因,并且易于修复。
例如,如果一个函数接受三个参数,则覆盖该函数的前两个参数之间的交互的单元测试可能没有注意为第三个参数提供有效值。重构测试后的代码可能会暴露出单元测试中的此缺陷,但是如果您了解代码应该执行的功能以及单元测试正在测试的内容,则很容易修复。
更改现有功能时,通常还需要更改某些单元测试。在这种情况下,单元测试有助于确保您的代码按预期更改功能,并且不会产生意外的副作用。
修复错误或添加新功能时,通常需要添加更多的单元测试。对于那些人,先提交单元测试然后再提交错误修复或新功能会很有帮助。这样可以更轻松地验证新的单元测试没有通过较旧的代码通过,而是通过了较新的代码通过。但是,这种方法并非完全没有缺陷,因此也存在支持同时提交新的单元测试和代码更新的争论。
最好将时间花在涉及用例的集成测试上,这会使范围较小的测试变得不那么重要。
这有一定道理。如果通过针对软件堆栈较高层的测试可以覆盖软件堆栈的较低层,则在重构代码时,测试可能会更有帮助。
我认为您不会在单元测试和集成测试之间的确切区别上达成共识。而且,如果您有一个测试用例,一个开发人员将其称为单元测试,而另一个称为集成测试,则无需担心,只要他们可以同意这是一个有用的测试用例即可。
进行集成测试以检查系统是否按照预期的方式运行,这始终具有业务价值。单元测试并非总是如此。例如,单元测试可以测试无效代码。
有一种测试哲学说,任何不给您信息的测试都是浪费。当不处理一段代码时,它的单元测试总是(或几乎总是)变成绿色,给您的信息很少或没有。
每种测试方法都不完整。即使您通过某种指标获得了100%的覆盖率,您仍然不会遍历几乎所有可能的输入数据集。因此,不要认为单元测试是魔术。
我经常看到有人声称进行单元测试可以改善您的代码。在我看来,单元测试给代码带来的改进是迫使不习惯它的人们将代码分解成小巧的,结构合理的片段。如果您习惯于通常以小巧,结构合理的方式设计代码,则可能会发现您不再需要单元测试来强迫您这样做。
根据单元测试的强度和程序的规模,最终可能会导致大量的单元测试。如果是这样,大的改变就变得更加困难。如果较大的更改导致大量的单元测试失败,则可以拒绝进行更改,进行大量的工作来修复大量的测试或放弃测试。没有一个选择是理想的。
单元测试是工具箱中的工具。他们在那里为您服务,而不是相反。确保从其中取出至少与放入的一样多的东西。
单元测试绝对不是某些支持者声称的灵丹妙药。但总的来说,它们的成本/收益比非常高。它们不会使您的代码“更好”,甚至不会更加模块化。“更好”具有比“可测试性”所暗示的更多的因素。但是他们将确保它在您考虑的所有情况和用法中都有效,并且将消除您的大多数错误(可能是更琐碎的错误)。
单元测试的最大好处是,它们使您的代码更灵活,更灵活地进行更改。您可以重构或添加功能,并确信自己没有破坏任何东西。仅此一项就使它们对大多数项目都值得。
提及您的朋友的观点:
如果添加POJO破坏了单元测试,则它们可能写得很差。POJO通常是您用来谈论域的语言,解决的问题很明显,更改它们意味着您的整个业务逻辑,而且表示代码可能必须更改。您不能指望什么能确保程序的那些部分不会改变...
抱怨单元测试很糟糕,因为人们将它们注释掉了,就像抱怨安全带不好是因为人们不戴安全带。如果有人注释掉测试代码并破坏了某些内容,那么他就应该与老板进行非常严肃而令人不愉快的谈话。
集成测试远胜于单元测试。没有问题。尤其是在测试产品时。但是编写好的,全面的集成测试将很难给您带来与单元测试相同的价值,覆盖率和正确性保证。
我只能想到一个反对单元测试的论点,这很弱。
当您在以后的阶段重构或更改代码时,单元测试将显示它们的大部分价值。您会发现添加功能并确保没有损坏任何东西所需的时间大大减少。
但是:首先,在编写测试时要付出(小的)代价。
因此:如果一个项目足够短并且不需要持续的维护/更新,那么编写测试的成本可能会超过其收益。