受此线程的触发,我(再次)正在考虑最终在项目中使用单元测试。那里的一些海报说“如果测试是好的,那么测试很酷”。我现在的问题是:什么是“好”测试?
在我的应用程序中,主要部分通常是某种数值分析,具体取决于大量观察到的数据,并得出可用于对这些数据进行建模的拟合函数。我发现很难为这些方法构建测试,因为可能的输入和结果的数量太大,无法仅对每种情况进行测试,而且这些方法本身通常很长,在不牺牲性能的情况下不易重构。我对这种方法的“良好”测试特别感兴趣。
受此线程的触发,我(再次)正在考虑最终在项目中使用单元测试。那里的一些海报说“如果测试是好的,那么测试很酷”。我现在的问题是:什么是“好”测试?
在我的应用程序中,主要部分通常是某种数值分析,具体取决于大量观察到的数据,并得出可用于对这些数据进行建模的拟合函数。我发现很难为这些方法构建测试,因为可能的输入和结果的数量太大,无法仅对每种情况进行测试,而且这些方法本身通常很长,在不牺牲性能的情况下不易重构。我对这种方法的“良好”测试特别感兴趣。
Answers:
单元测试的艺术有以下说关于单元测试:
单元测试应具有以下属性:
- 它应该是自动化且可重复的。
- 它应该易于实现。
- 写入后,应保留以备将来使用。
- 任何人都应该能够运行它。
- 只需按一下按钮即可运行。
- 它应该可以快速运行。
然后再添加,它应该是完全自动化,可信赖,可读和可维护的。
如果您还没有的话,我强烈建议您阅读这本书。
在我看来,所有这些都是非常重要的,但尤其是后三个(可信任,可读和可维护),就像您的测试具有这三个属性一样,您的代码通常也具有它们。
It should run at the push of a button
,这是否意味着单元测试不应要求容器(应用服务器)正在运行(针对被测试的单元)或资源连接(例如DB,外部Web服务等)?对于应用程序的哪些部分应该进行单元测试,哪些不应该进行单元测试,我感到困惑。有人告诉我,单元测试不必依赖数据库连接和运行容器,而可以使用模型代替。
一个好的单元测试不能反映它正在测试的功能。
作为一个大大简化的示例,请考虑您有一个平均返回两个int的函数。最全面的测试将调用该函数并检查结果是否实际上是平均值。这根本没有任何意义:您正在镜像(复制)要测试的功能。如果您在主功能上犯了一个错误,那么您在测试中也会犯同样的错误。
换句话说,如果您发现自己在单元测试中复制了主要功能,则可能是您在浪费时间。
好的单元测试本质上是可运行形式的规范:
我发现Test-Driven-Development非常适合于库例程,因为您本质上是先编写API,然后再编写实际的实现。
对于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
现在,您可能已经注意到,故事中描述的特定情况没有解决方案。事实证明,在与客户和其他利益相关者交谈之后,原始故事中的目标只是一个假设的例子。真正的测试来自随后的讨论。这可能发生。故事应该被重写,但是不必(因为故事只是与客户交谈的占位符)。
我已经看到很多情况下,人们花费大量的精力为很少输入的代码编写测试,而不是为频繁输入的代码编写测试。
在坐下来编写任何测试之前,您应该查看某种调用图,以确保计划足够的覆盖范围。
另外,我不相信仅仅为了说“是的,我们测试那个”而编写测试。如果我使用的是一个插入式库并且将保持不变,那么我就不会浪费一天的时间来编写测试以确保永远不会改变的API内幕都能按预期工作,即使它的某些部分得分很高在通话图上较高。该测试消耗说库(我自己的代码)指出这一点。
我尝试让每个测试仅测试一件事。我尝试给每个测试一个名称,如shouldDoSomething()。我尝试测试行为,而不是实现。我只测试公共方法。
通常,对于每种公共方法,我通常都会进行一次或几次成功测试,然后可能会进行一系列失败测试。
我经常使用实体模型。一个好的模拟框架可能会很有帮助,例如PowerMock。虽然我还没用。
如果类A使用另一个类B,我将添加一个接口X,以便A不直接使用B。然后,我将创建模型XMockup并在测试中使用它代替B。它确实有助于加快测试执行速度,降低测试复杂性,并减少了我为A编写的测试数量,因为我不必应对B的特性。例如,我可以测试A调用X.someMethod()而不是调用B.someMethod()的副作用。
还要保持测试代码的清洁。
当使用API(例如数据库层)时,我会对其进行模拟并启用模型以在命令中的每一个可能的机会上引发异常。然后,我一次不抛出就运行测试,然后循环运行,每次在下一次机会抛出异常,直到测试再次成功。有点像Symbian可用的内存测试。