根据有用性进行单元测试的类型


13

从价值的角度来看,我在实践中看到两组单元测试:

  1. 测试一些非平凡逻辑的测试。(在实现之前或之后)编写它们会发现一些问题/潜在错误,并有助于您确定将来逻辑发生变化时的情况。
  2. 测试一些非常琐碎的逻辑的测试。这些测试比测试代码更像文档代码(通常带有模拟)。这些测试的维护工作流不是“某些逻辑改变了,测试变成了红色-感谢上帝,我编写了这个测试”,而是“一些琐碎的代码改变了,测试变成了假否定-我必须维护(重写)该测试而没有获得任何收益” 。在大多数情况下,这些测试不值得维护(出于宗教原因除外)。根据我在许多系统中的经验,这些测试占所有测试的80%。

我正在尝试找出其他人对单元测试按值分离以及如何与我的分离相对应的想法。但是我最常看到的是全职TDD宣传或测试都是无用的,只是编写代码宣传。我对中间的东西感兴趣。欢迎您发表自己的想法或参考文章/论文/书籍。


3
我一直在单元测试中检查已知(特定)的错误-这些错误曾经通过原始的单元测试集-作为一个单独的组,其作用是防止回归错误。
Konrad Morawski 2014年

6
我认为那些第二种测试是一种“变化摩擦”。不要低估它们的用处。甚至更改琐碎的代码也往往会在整个代码库中产生连锁反应,而引入这种摩擦会阻碍开发人员的工作,使他们只能更改真正需要它的内容,而不是根据某些异想天开或个人喜好进行更改。
Telastyn 2014年

3
@Telastyn -对您的评论的一切似乎完全疯了我。谁会故意使更改代码变得困难?为什么不鼓励开发人员更改他们认为合适的代码-您不信任他们吗?他们是不好的开发商吗?
本杰明·霍奇森

2
无论如何,如果更改代码趋于产生“涟漪效应”,那么您的代码就会遇到设计问题 -在这种情况下,应鼓励开发人员尽可能合理地进行重构。脆弱的测试会积极地阻碍重构(测试失败;谁会费心找出该测试是否是80%的测试中没有真正做任何事情的测试之一?您只是找到了另一种更复杂的方法)。但是您似乎将其视为理想的特性……我一点都不明白。
本杰明·霍奇森

2
无论如何,OP可能会发现Rails的创建者的这篇博客文章很有趣。为了大大简化他的观点,您可能应该尝试丢弃那些80%的测试。
本杰明·霍奇森

Answers:


14

我认为在单元测试中遇到分歧是很自然的。关于如何正确执行操作,存在许多不同的意见,自然所有其他意见本质上都是错误的。最近在DrDobbs上有很多文章探讨了这个问题,我在答案的结尾将其链接到该文章。

我在测试中看到的第一个问题是很容易弄错它们。在我的大学C ++课程中,我们在第一学期和第二学期都接受了单元测试。在这两个学期中,我们对编程的总体知识一无所知-我们试图通过C ++学习编程的基础知识。现在,假设对学生说:“哦,您写了个年度税计算器!现在编写一些单元测试以确保其正常工作。” 结果应该是显而易见的-包括我的尝试在内,它们都是可怕的。

一旦您承认自己精于编写单元测试并希望获得更好的成绩,您很快就会面临时髦的测试风格或不同的方法。通过测试方法,我指的是诸如测试优先或DrDobbs的Andrew Binstock所做的做法,即在代码旁边编写测试。两者都有其优点和缺点,我拒绝赘述任何主观细节,因为这会引发火焰战争。如果您不对哪种编程方法更好感到困惑,那么也许可以通过测试风格解决问题。您应该使用TDD,BDD,基于属性的测试吗?JUnit具有称为理论的高级概念,该概念模糊了TDD和基于属性的测试之间的界限。什么时候使用?

tl; dr容易出错,这是令人难以置信的观点,并且我认为,只要在适合的情况下勤奋和专业地使用它们,任何一种测试方法本质上都不会更好。在我看来,断言或理智测试的扩展曾经用于确保快速,快速,即席的临时开发方法,现在这要容易得多。

对于主观意见,我宁愿编写测试的“阶段”,因为缺少更好的短语。我编写了单元测试来隔离测试类,并在必要时使用模拟。这些可能会用JUnit或类似的东西执行。然后,我编写集成或验收测试,它们分别运行,通常一天仅运行几次。这些是您不平凡的用例。我通常使用BDD,因为用自然语言表达功能非常好,这是JUnit无法轻松提供的功能。

最后,资源。这些将提出相互矛盾的意见,这些意见主要围绕使用不同语言和不同框架的单元测试。他们应该表现出意识形态和方法论上的分歧,同时只要我还没有过多地操纵你的意见,就可以让你发表自己的看法:)

[1] 安德鲁·宾斯托克(Andrew Binstock)的敏捷腐败

[2] 对上一篇文章的回应

[3] Bob叔叔对敏捷腐败的回应

[4] 罗布·迈尔斯(Rob Myers)对敏捷腐败的回应

[5] 为什么要打扰黄瓜测试?

[6] 您误解了

[7] 远离工具

[8] 关于“带有注释的罗马数字卡塔”的评论

[9] 带有注释的罗马数字Kata


1
我的一个友好论点是,如果您要编写测试年税计算器功能的测试,那么您就不是编写单元测试。那是一个集成测试。应该将您的计算器分解为相当简单的执行单元,然后您的单元测试将测试这些单元。如果这些单元之一停止正常运行(测试开始失败),则就像击倒基础墙的一部分一样,您需要修复代码(通常不是测试)。要么,要么您已经确定了一些不再需要的代码,应该将其丢弃。
Craig

1
@克雷格:准确!这就是我不知道如何编写适当的测试的意思。作为一名大学生,收税员是一门大课,写作时对SOLID没有正确的了解。您是绝对正确的想法,这比其他任何测试都更像是一个集成测试,但这对我们来说是个未知数。我们的教授只接受了“单元”测试。
IAE 2014年

5

我认为,必须同时进行两种类型的测试并在适当的地方使用它们,这一点很重要。

就像您说的那样,有两个极端,老实说,我也不同意任何一个。

关键是单元测试必须涵盖业务规则和要求。如果要求系统必须跟踪一个人的年龄,请编写“琐碎的”测试以确保年龄是一个非负整数。您正在测试系统所需的数据域:虽然微不足道,但它具有一定的价值,因为它正在执行系统的参数

同样,对于更复杂的测试,它们也必须带来价值。当然,您可以编写一个测试来验证不是必需的东西,而是应该在某个地方的象牙塔中强制执行的测试,但这是花费更多的时间来编写用于验证客户为您付款的要求的测试。例如,为什么当唯一的流来自本地文件而不是网络时,为什么编写一个验证代码的测试可以处理超时的输入流?

我坚信单元测试,并在任何有意义的地方使用TDD。在更改代码时,单元测试肯定会以提高质量和“快速失败”行为的形式带来价值。但是,也要记住旧的80/20规则。在某些时候,编写测试时您的收益将会递减,即使编写更多测试有一定的可测量价值,您也需要转而从事更具生产力的工作。


编写测试以确保系统可以跟踪人的年龄不是IMO的单元测试。那是一个集成测试。单元测试将测试通用的执行单元(也称为“过程”),例如,它根据基准日期和任何单位(天,周等)的偏移量来计算年龄值。我的观点是,该代码段对系统的其余部分不应有任何奇怪的外在依赖。它只是根据几个输入值来计算年龄,在这种情况下,单元测试可以确认正确的行为,如果偏移量产生负的年龄,则可能抛出异常。
Craig 2014年

我指的不是任何计算。如果模型存储一条数据,则可以验证该数据是否属于正确的域。在这种情况下,域是非负整数的集合。计算应在控制器中进行(在MVC中),在此示例中,寿命计算将是单独的测试。

4

这是我的看法:所有测试都有成本:

  • 最初的时间和精力:
    • 考虑要测试什么以及如何测试
    • 实施测试并确保它正在测试应该执行的操作
  • 持续维护
    • 确保测试仍在按照代码自然发展的方式进行预期的工作
  • 运行测试
    • 执行时间处理时间
    • 分析结果

我们还打算使所有测试都能带来收益(据我的经验,几乎所有测试都能带来收益):

  • 规格
  • 突出显示极端情况
  • 防止回归
  • 自动验证
  • API使用示例
  • 量化特定属性(时间,空间)

因此,很容易看到,如果您编写一堆测试,它们可能会有价值。当您开始比较该值(顺便说一下,您可能事先不知道,如果您丢弃代码,则回归测试会失去其值)与成本之间的关系将变得更加复杂。

现在,您的时间和精力有限。您想选择做那些以最少的成本提供最大收益的事情。我认为这是一件非常困难的事情,尤其是因为它可能需要知识,而这些知识是没有的,或者获取起来会很昂贵。

这就是这些不同方法之间的真正摩擦。我相信他们都已经确定了有益的测试策略。但是,每种策略通常都有不同的成本和收益。同样,每种策略的成本和收益可能在很大程度上取决于项目,领域和团队的具体情况。换句话说,可能有多个最佳答案。

在某些情况下,未经测试就抽出代码可能会提供最佳收益/成本。在其他情况下,全面的测试套件可能会更好。在其他情况下,改进设计可能是最好的选择。


2

什么一个单元测试,真的吗?这里真的有这么大的二分法吗?

我们在这样一个领域中工作:从缓冲区末尾读取一点点的字面量可能会使程序完全崩溃,或导致程序产生完全不正确的结果,或者如最近的“ HeartBleed” TLS错误所证明的那样,在整个系统范围内放置了一个安全的虚拟机。打开时不会产生任何直接的缺陷证据。

从这些系统中消除所有复杂性是不可能的。但是我们的工作是尽可能地减少和管理这种复杂性。

单元测试是否是一种测试,例如,它可以确认在三个不同的系统中成功发布了预订,创建了日志条目并发出了电子邮件确认?

我要说。那是一个集成测试。那些绝对肯定有他们的位置,但是它们也是一个不同的主题。

集成测试旨在确认整个“功能”的总体功能。但是,该功能背后的代码应分解为简单的,可测试的构建块,也称为“单元”。

因此,单元测试的范围应非常有限。

这意味着单元测试测试的代码应具有非常有限的范围。

这进一步意味着,良好设计的支柱之一是将您的复杂问题分解为较小的,单一用途的部件(在可能的范围内),这些部件可以相对隔离地进行测试。

最终得到的是一个由可靠的基础组件组成的系统,并且您知道这些基础代码中的任何一个是否因为您编写了简单,小型,有限范围的测试来告知您而导致破坏。

在许多情况下,您可能还应该每个单元进行多个测试。测试本身应该是简单的,尽可能地测试一种和一种行为。

我认为,测试非平凡,复杂,复杂的逻辑的“单元测试”的概念有点矛盾。

因此,如果发生了这种故意的设计故障,那么除非被测代码单元的基本功能发生了变化,否则单元测试在世界上怎么会突然开始产生误报?如果发生了这种情况,那么您最好相信其中存在一些非显而易见的涟漪效应。您的破损测试(似乎正在产生误报)实际上是在警告您,某些更改破坏了代码库中更广泛的依赖关系,需要对其进行检查和修复。

其中一些单元(其中许多单元)可能需要使用模拟对象进行测试,但这并不意味着您必须编写更复杂或更复杂的测试。

回到我的人为保留系统的示例,您确实无法在每次单元测试代码时都将请求发送到实时保留数据库或第三方服务(甚至是其“开发”实例)。

因此,您可以使用提供相同接口协定的模拟程序。然后,测试可以验证相对较小的确定性代码块的行为。木板下面的所有绿色都告诉您构成基础没有损坏。

但是各个单元测试本身的逻辑仍然尽可能简单。


1

当然,这只是我的观点,但是过去几个月来用fsharp学习函数式编程(来自C#背景)使我意识到了一些事情。

正如OP所述,我们通常每天会看到两种类型的“单元测试”。覆盖方法内幕的测试通常是最有价值的,但对于系统的80%而言却很难做到,这与“算法”无关,而与“抽象”有关。

另一种类型是测试抽象交互性,通常涉及模拟。在我看来,由于您的应用程序的设计,大多数测试是必需的。省略它们,您将冒着怪异的bug和Spagetti代码的风险,因为人们不会正确考虑他们的设计,除非他们被迫首先进行测试(即使那样,通常会把它弄乱)。问题不只是测试方法,而是系统的基础设计。大多数使用命令式或OO语言构建的系统都固有地依赖于“副作用”,也就是“这样做,但是什么都不要告诉我”。当您依赖副作用时,您需要对其进行测试,因为业务需求或运营通常是其中的一部分。

当您以更实用的方式设计系统时,避免在副作用上建立依赖关系,并通过不变性避免状态更改/跟踪,它使您可以更专注于“内外”测试,从而清楚地测试更多操作,以及到达那里的方式。对于相同的问题,不变性之类的东西可以为您提供更简单的解决方案,您会感到惊讶。当您不再依赖“副作用”时,您几乎可以执行并行化和异步编程之类的事情,而几乎不会增加任何成本。

自从我开始使用Fsharp进行编码以来,我不需要任何东西的模拟框架,甚至完全摆脱了对IOC容器的依赖。我的测试是由业务需求和价值驱动的,而不是在命令式编程中通常需要的繁重抽象层上进行的。

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.