it-swarm.cn

如何编写“好的”单元测试?

此线程 触发,我(再次)正在考虑最终在项目中使用单元测试。那里的一些海报说“如果测试是好的,测试就是很酷的”。我现在的问题是:什么是“好”测试?

在我的应用程序中,主要部分通常是某种数值分析,具体取决于大量观察到的数据,并产生可用于对该数据建模的拟合函数。我发现为这些方法构建测试特别困难,因为可能的输入和结果的数量太大,无法仅对每种情况进行测试,而且这些方法本身通常很长,在不牺牲性能的情况下不易重构。我对这种方法的“良好”测试特别感兴趣。

63
Jens

单元测试的技巧 对于单元测试有以下说法:

单元测试应具有以下属性:

  • 它应该是自动化且可重复的。
  • 它应该易于实现。
  • 写入后,应保留以备将来使用。
  • 任何人都应该能够运行它。
  • 只需按一下按钮即可运行。
  • 它应该可以快速运行。

然后再添加,它应该是完全自动化,可信赖,可读和可维护的。

如果您还没有的话,我强烈建议您阅读这本书。

在我看来,所有这些都是非常重要的,但尤其是后三个(可信任,可读和可维护),就像您的测试具有这三个属性一样,您的代码通常也具有它们。

52
Andy Lowry

一个好的单元测试不能反映它正在测试的功能。

作为一个大大简化的示例,请考虑您有一个平均返回两个int的函数。最全面的测试将调用该函数并检查结果是否实际上是平均值。这根本没有任何意义:您正在镜像(复制)要测试的功能。如果您在主功能上犯了一个错误,那么您在测试中也会犯同样的错误。

换句话说,如果您发现自己在单元测试中复制了主要功能,则很可能表明您正在浪费时间。

43
mojuba

好的单元测试本质上是可运行形式的规范:

  1. 描述与用例相对应的代码的行为
  2. 涵盖技术性极端情况(如果通过null会发生什么情况)-如果不存在针对极端情况的测试,则行为未定义。
  3. 如果测试的代码偏离规范,则中断

我发现Test-Driven-Development非常适合于库例程,因为您本质上是先编写API,然后再编写实际的实现。

10
user1249

对于TDD,“良好”测试用于测试客户想要的功能;功能不一定与功能相对应,开发人员不应在真空中创建测试场景

在您的情况下-我正在猜测-“功能”是fit函数在一定的误差容限内对输入数据进行建模。因为我不知道你在做什么,所以我在编造一些东西。希望它是镇痛药。

示例故事:

作为[X机翼飞行员],我希望[不超过0.0001%拟合误差],以便[目标计算机在全速通过箱形峡谷时可以击中死星的排气口]

因此,您可以与飞行员交谈(如果有感觉,也可以与目标计算机交谈)。首先,您谈论什么是“正常”,然后谈论异常。您会发现在这种情况下真正重要的是什么,什么是共同的,什么是不可能的,什么是唯一的可能。

假设通常在遥测数据的七个通道上有一个半秒的窗口:速度,俯仰,侧倾,偏航,目标矢量,目标尺寸和目标速度,并且这些值将保持不变或线性变化。通常,您的频道可能较少,并且/或者这些值可能正在快速变化。所以together您提出了一些测试,例如:

//Scenario 1 - can you hit the side of a barn?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is zero
    and all other values are constant,
Then:
    the error coefficient must be zero

//Scenario 2 - can you hit a turtle?
Given:
    all 7 channels with no dropouts for the full half-second window,
When:
    speed is zero
    and target velocity is less than c
    and all other values are constant,
Then:
    the error coefficient must be less than 0.0000000001/ns

...

//Scenario 42 - death blossom
Given:
    all 7 channels with 30% dropout and a 0.05 second sampling window
When:
    speed is zero
    and position is within enemy cluster
    and all targets are stationary
Then:
    the error coefficient must be less than 0.000001/ns for each target

现在,您可能已经注意到,故事中描述的特定情况没有解决方案。事实证明,在与客户和其他利益相关者交谈之后,原始故事中的目标只是一个假设的例子。真正的测试来自随后的讨论。这可能发生。故事应该被重写,但不必(因为故事只是与客户对话的占位符)。

7
Steven A. Lowe

为极端情况创建测试,例如仅包含最小输入数量(可能为1或0)和一些标准情况的测试集。这些单元测试不能替代彻底的验收测试,也不能替代。

5
user281377

我已经看到很多情况下,人们花费大量的精力为很少输入的代码编写测试,而不是为频繁输入的代码编写测试。

在坐下来编写任何测试之前,您应该查看某种调用图,以确保计划足够的覆盖范围。

另外,我不相信仅仅为了说“是的,我们进行测试”而编写测试。如果我使用的是一个插入式且仍保持不变的库,则我不会浪费一天的时间来编写测试以确保永远不会改变的API内幕都能按预期工作,即使它的某些部分得分很高在通话图上较高。测试consume所说的库(我自己的代码)指出了这一点。

5
Tim Post

TDD并非如此,但是进入质量检查之后,您可以通过设置测试用例来重现质量检查过程中出现的所有错误来改进测试。当您需要长期支持时,这会变得非常有价值,并且您开始遇到一个风险,即人们可能会不经意间重新引入旧错误。进行测试以捕获这一点特别有价值。

4
glenatron

我尝试让每个测试仅测试一件事。我尝试给每个测试一个名称,如shouldDoSomething()。我尝试测试行为,而不是实现。我只测试公共方法。

通常,对于每种公共方法,我通常都会进行一次或几次成功测试,然后可能会进行一系列失败测试。

我经常使用实体模型。一个好的模拟框架可能会很有帮助,例如PowerMock。虽然我还没有用。

如果A类使用另一个B类,则添加一个接口X,这样A不会直接使用B。然后,我将创建模型XMockup并在测试中使用它代替B。它确实有助于加快测试执行速度,降低测试复杂性,并减少了我为A编写的测试数量,因为我不必应对B的特性。例如,我可以测试A调用X.someMethod()而不是调用B.someMethod()的副作用。

还要保持测试代码的清洁。

当使用诸如数据库层之类的API时,我会对其进行模拟,并使模型能够在命令的每一个可能的机会上抛出异常。然后,我一次不抛出就运行测试,然后循环运行,每次在下一次机会抛出异常,直到测试再次成功。有点像Symbian可用的内存测试。

3
Roger C S Wernersson

我看到Andry Lowry已经发布了Roy Osherove的单元测试指标;但是似乎没有人提出Bob叔叔在Clean Code(132-133)中提供的(免费)套装。他使用首字母缩写词FIRST(此处是我的摘要):

  • 快速(它们应该快速运行,所以人们不会介意运行它们)
  • 独立(测试不应相互进行设置或拆卸)
  • 可重复(应在所有环境/平台上运行)
  • 自我验证(完全自动化;输出应为“通过”或“失败”,而不是日志文件)
  • 及时(何时编写,就在编写要测试的生产代码之前)
2
Kazark