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


61

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

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


8
任何好的单元测试都只能测试一件事-如果失败,您应该确切知道出了什么问题。
gablin

2
当拥有大量数据时,好处是编写可以将数据文件作为输入的通用测试。数据文件通常应同时包含输入和预期结果。使用xunit测试框架,您可以即时生成测试用例-每个数据样本一个。
froderik

2
@gablin“如果失败,您应该确切知道出了什么问题”,这表明具有多个可能失败原因的测试是可以的,只要您可以从测试的输出中确定原因即可?
user253751

似乎没有人提到单元测试可以测试操作需要多长时间。您可以考虑性能来重构代码,确保单元测试根据时间和结果告诉您它是通过还是失败。
CJ丹尼斯

Answers:


52

单元测试的艺术有以下说关于单元测试:

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

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

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

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

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


1
+1获取针对单元测试(而非集成或功能测试)的完整列表
Gary Rowe 2010年

1
+1的链接。有趣的材料在那里找到。
Joris Meys 2011年

1
“快速运行”具有重大意义。这是单元测试应该隔离运行的原因之一,它要远离数据库,文件系统,Web服务等外部资源。这又导致了模拟/存根。
迈克尔·复活节

1
当说时It should run at the push of a button,这是否意味着单元测试不应要求容器(应用服务器)正在运行(针对被测试的单元)或资源连接(例如DB,外部Web服务等)?对于应用程序的哪些部分应该进行单元测试,哪些不应该进行单元测试,我感到困惑。有人告诉我,单元测试不必依赖数据库连接和运行容器,而可以使用模型代替。
两栖游戏,2016年

42

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

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

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


21
+1在这种情况下,您需要对硬编码的参数进行测试,然后根据已知答案进行检查。
Michael K

我以前见过那种味道。
保罗·布彻

您能否为返回平均值的函数提供一个良好的单元测试示例?
VLAS 2015年

2
@VLAS测试预定义值,例如确保avg(1,3)== 2,更重要的是检查边缘情况,例如INT_MAX,零,负值等。如果在函数中发现并修复了错误,请添加另一个测试以确保永远不会引入此错误。
mojuba 2015年

有趣。您如何建议为这些测试输入获得正确答案,而又不犯与要测试的代码相同的错误?
蒂莫,2016年

10

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

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

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


7

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

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

示例故事:

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

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

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

//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

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


5

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


5

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

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

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


但是在以后的库中有较新版本的错误修复又会如何呢?

@ThorbjørnRavn Andersen-这取决于库,更改的内容以及他们自己的测试过程。当我将代码放到适当位置并且永不碰触时,我不会为我知道可以工作的代码编写测试。因此,如果它在更新后仍然有效,那就没关系了:)当然也有例外。
Tim Post

如果您依赖于库,那么您至少可以做的是编写测试,以显示您期望该库实际执行的测试

...如果改变了,就测试消耗所述库的事物... tl; dr; 我不需要测试第三方代码的内在特性。为了清楚起见,答案已更新。
蒂姆·波斯特

4

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


3

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

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

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

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

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

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


2

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

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