原文:
5 Questions Every Unit Test Must Answer
每个开发者都知道我们应该编写单元测试以防程序上到生产环境才检测到错误。
大多数开发者都不知道编写单元测试的基本要素。我无法开始计算我看过单元测试失败的次数,只有经过调查我才发现我完全不知道开发人员试图测试哪些功能,更不用说它出了什么问题或者为什么它很重要。
在最近的一个项目中,我们让一大堆单元测试进入测试单元,完全没有描述测试目的。我们有一只很棒的团队,所以我放松了警惕。结果?我们仍然有大量的单元测试,只有作者才能真正理解。
幸运的是,我们正在重新设计API,我们将把整个测试单元重新开始,否则,这将成为我的修复列表中的第一优先级任务。
不要让这发生在你身上。
是什么影响了测试的规则?
你的测试是你抵御软件缺陷的第一道防线。你的测试比linting和静态分析(它只能找到一个错误的子类,而不是你的实际程序逻辑的问题)更重要。测试与实现本身同样重要。
单元测试结合了许多功能,使它们成为应用程序成功的秘密武器。
- 设计帮助: 首先编写测试可以让您更清楚地了解理想的API设计
- 功能文档(针对开发人员): 每个实现的功能需求都包含在代码中的测试说明。
- 测试开发人员的理解程度: 开发人员是否对这个问题理解到位,从代码中所有关键部分的阐述便能看出。
- 质量保证: 手动QA容易出错。根据我的经验,在更改、添加或删除功能后,开发人员不可能记住所有需要测试的功能。
- 持续交付: 自动化QA提供了自动防止部署到生产环境中破损构建的机会。
单元测试不需要曲解操纵来满足所有这些广泛的目标。相反,单元测试的本质是满足所有这些需求。这些好处都是一个精心编写的并有良好覆盖率的测试单元的副作用。
TDD科学(Test-driven development)
证据表明
- TDD可以减少bug的密度
- TDD可以鼓励更多的模块化设计(增强软件敏捷性/团队开发速度)
- TDD可以降低代码的复杂度
先写测试
来自微软研究院,IBM和Springer的研究测试了,先编写测试和后编写测试的效果,并始终发现,测试优先的流程比稍后添加测试能产生更好的效果。它非常明确:在编写代码之前,先编写测试
什么是良好的单元测试?
怎样才能编写一个良好的单元测试?
我们将从一个真正的项目看一个非常简单的例子来探索这个过程:来自Stamp Specification的compose()
函数。
我们将使用tape进行单元测试,因为它足够清晰简单。
在我们能够回答编写好的单元测试之前,首先我们必须了解如何使用单元测试:
- 设计帮助: 在编写代码之前的设计阶段编写
- 功能文档&测试开发者的理解程度: 测试应该提供被测功能的清晰描述
- 质量保证/持续交付: 测试应当在发生故障时停止交付管道,并在失败时产生一份好的错误报告。
将单元测试作为测试报告
当测试失败,那个测试失败报告通常是您确切发现错误的第一个也是做好的线索-快速追踪根本原因的秘密是知道从哪里开始寻找。当你有一个非常清晰的错误报告时,这个过程变得更容易。
什么是好的测试失败报告
- 你在测试什么?
- 它应该做什么?
- 输出是什么(实际行为)?
- 期望输出是什么(期望行为)?
从回答"你在测试什么?"开始
- 你在测试组件的什么方面?
- 该功能应该做什么?你正在测试什么特定的行为要求?
compose()
函数使用任意数量的邮票(可组合的工厂函数)并生成一个新邮票。
为了编写这个测试,我们将从任何单个测试的最终目标开始向后工作:测试特定的行为要求。为了通过这个测试,代码必须产生什么样的特定行为?
该功能应该做什么?
我喜欢从写一个字符串开始。没有分配给任何东西。没有传入任何函数。只是清楚地记关注组件必须满足的特定要求。在这种情况下,我们将从compose()
函数应该返回一个函数的事实开始。
一个简单的,可测试的要求
'compose() should return a function.'
现在我们将跳过一些东西,并充实剩下的测试。这个字符串是我们的目标。事先陈述有助于我们保持对最终结果的关注。
你正在测试什么组件的那一方面?
组件各个方面的含义因测试而异,具体取决于为测试组件提供足够覆盖度所需的粒度。
在上面的例子中,我们将测试compose()
函数的返回类型,以因确保它返回正确的类型,而不是在运行时抛出'undefined'或什么都不抛出。
让我们将这个问题转换为测试代码。答案进入测试描述。这一步也是我们进行函数调用并将回调函数传递给测试运行器时,它在测试运行时会调用回调函数的地方。
test('<What component aspect are we testing?>', assert => {});
在这个例子中,我们将测试compose
函数的输出
test('Compose function output type.', assert => {
})
我们当然也需要我们最开始的描述。它将放进我们的回调函数:
test('Compose function output type', assert => {
'compose() should return a function.'
})
输出是什么(期望的或实际的)?
equal()
是我最喜欢的断言。如果每个测试单元唯一可能的断言是equal()
,那么世界上几乎所有的测试单元都会更好。为什么?
因为equal()
,自然的回答每个测试单元都回答的两个重要问题,但是大多数没有:
- 什么是实际的输出?
- 什么是期望的输出?
如果你完成了测试但没有回答这两个问题,你并没有在进行一个真正的单元测试。你只是有一个马虎的,不成熟的测试。
如果你从这篇文章中只看出一件事,那这件事就是: Equal
是你的新的默认的断言。它是每个优秀测试单元的主要组成部分。所有拥有数百种花哨不同断言的断言库正在破坏你的测试质量。
一个挑战
希望在编写单元测试时更好?下一周,尝试使用equal
或deepEqual
来编写每一个断言,或者在你的断言库中有差不多的选择。不要担心它的质量会影响你的单元测试。我的收入告诉你这种训练会显著的提高你的单元测试。
下面的代码看起来像什么?
const actual = '<what is the actual output?>';
const expected = '<what is the expected output?>';
第一个问题确实是在测试失败中承担双重责任。通过回答这个问题,你的代码也可以回答另外一个问题:
const actual = '<how is the test reproduced>';
需要注意的很重要的点是,actual
值必须在使用组件公共API时被生产出来。否则,测试毫无意义。
让我们回到例子:
const actual = typeof compose();
const expected = 'function';
你可以构建一个断言,而不用专门赋值给名为actual
和expected
的变量,但是我最近开始在每个测试中专门为变量actual
和expected
赋值,并发现它使我的测试更易于阅读。
看这个它是如何使的这个断言清晰的?
assert.equal(actual, expected,
'compose() should returns a function';
)
它在测试代码中,分离了how和what
- how:即指我们是如何获得这个值的?看看变量赋值。
- what:即指我们正在测试什么?看看断言描述吧。
阅读测试的结果应该和阅读一个高质量的bug报告一样。
我们来看看上下文中的所有内容:
import test from 'tape';
import compose from '../source/compose';
test('Compose function output type', assert => {
const actual = typeof compose();
const expected = 'function';
assert.equal(actual, expected, 'compose() should return a function');
assert.end();
})
下一次,你编写测试代码,记得回答下面所有的问题:
- 你在测试什么?
- 它应该做什么?
- 实际的输出是什么?
- 期望的输出是什么?
- 如何再现测试?
最后的问题由用于导出actual
值得代码回答。
一个单元测试模板
import test from 'tape';
// For each unit test your write,
// answser these questions
test('What component aspect are you testing?', assert => {
const actual = 'What is the actual output?';
const expected = 'What is the expected output?';
assert.equal(actual, expected,
'what should the feature do?');
assert.end();
});
还有很多关于单元测试的使用案例,但知道如何编写一个好的测试还有很长的路要走。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。