原文地址: medium.com/javascript-…
译文地址:github.com/xiao-T/note…
本文版权归原作者所有,翻译仅用于学习。
大多开发者不知道如何测试
为了避免对生产环境带来影响,每个开发者都知道需要编写单元测试。
大多数开发者并不知道每个单元测试的基本组成部分。我不记得见过多少次失败的单元测试,调查发现我无法理解开发者想测试什么,更不用说,它是如何错误的,或者为什么那么重要。
在我最近的一个项目中,我引入了大量的,没有任何描述说明的测试代码。我们是一个庞大的团队,因此,我放松了警惕。结果呢?我们有大量的单元测试只有对应的开发者自己明白干了什么。
很幸运,我们完全重新设计了 API,我们完全抛弃了之前的方案,从头开始;也就是说,这将是我最重要的事情。
不要让这些事情发生在你身上。
为什么要单元测试呢?
测试是抵御软件缺陷的第一道防线,也是最基本的防线。测试比 Lint 和静态分析(它们只能发现少量的错误,并不能发现程序中的逻辑问题)更加重要。测试和实现需求一样重要(最重要的是代码是否满足需求,除非实现的非常差,如何实现并不重要)。
单元测试包含了很多功能,这使它成了应用成功的秘密武器:
-
**辅助设计:**编写单元测试可以让你设计出更加清晰理想的 API。
-
**功能文档(针对开发者):**测试中包含了所有的功能需求。
-
**测试开发者对需求的理解:**开发者是否对所有的需求有足够的了解?以便实现更好的代码。
-
**质量保障:**手动 QA 容易出错。以我的经验,在重构、添加新功能或者移除功能后,开发者不能记住所有需要测试的功能点。
-
**辅助持续交付:**自动 QA 可以自动的避免破环生产环境。
单元测试并不是通过扭曲或者操纵来达到某些目的。相反,它本质上是为了满足需求。良好的单元测试可以带来更好的测试覆盖率。
TDD 科学
证据表明:
- TDD 可以减少 bug 的密度
- TDD 可以促进更好的模块化设计(提升软件的敏捷性/团队效率)
- TDD 可以减少代码的复杂度
科学证明: 有大量的经验证明 TDD 带来的好处
测试优先
来自微软、IBM 和 Springer 研究证明,一致的认为测试优先比延后测试可以产出更好的结果。现在非常的明确:在具体实现功能之前,需要先编写单元测试。
实现具体功能之前,
首先,需要编写单元测试
良好的单元测试应该是什么样子呢?
好,遵循 TDD 规则。先写测试。更规范。相信它...我知道了。但是如何编写一个良好的单元测试呢?
我们通过一个真实的场景来逐步探索:来自 Stamp 规范 的 compose()
方法
我们将会使用tape作为测试框架,因为它非常清晰简单。
在回答如何编写良好单元测试之前,首先,我们需要明白如何使用单元测试:
- **辅助设计:**在设计阶段编写测试,也就是说功能实现之前
- **功能文档 & 测试开发者对需求的理解:**单元测试应该对所测试的功能有一个清晰的描述
- **QA/持续交付:**单元测试在交付过程中失败的话,应该立即停止交付,并提供一个良好的 bug 报告
单元测试就是一个 Bug 报告
每当测试失败,失败报告都是有关错误的最好的线索 — 快速跟踪根本原因的秘密就是知道从哪开始。如果,你有一个清晰 bug 报告,这个过程会让你更加容易。
一个失败的单元测试看起来应该像是一个高质量的 bug 报告。
一个良好的错误报告应该包含哪些内容?
-
测试了什么内容?
-
应该是什么?
-
输出了什么(真实的行为)?
-
期望输出什么(期望的行为)?
开始回答:测试了什么内容?
- 你在测试组件的哪些内容?
- **功能应该是什么样的?**哪些特殊的行为需求你需要测试?
可以传递给函数 compose()
多个 stamps,然后,会产出一个新的 stamp。
为了编写测试用例,我们需要逆向思考:测试特定的行为需求。为了让测试通过,代码应该提供什么样的行为?
功能应该是什么样的?
我喜欢从一个字符串开始。不分配任何内容。不传递给任何函数。只是明确组件需要满足的特定功能需求。在这个示例中,我们将从 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 return a function.');
它将测试中的 “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 you write,
// answer 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();
});
有很多方式可以编写良好的单元测试,但是,如何编写良好的单元测试还有很长的路要走。
下一步
加入 TDD Day 可以观看很多有关 TDD 的直播课程。TDD Day 可以带领你的团队了解更多有关 TDD 的高级技能。学习各种测试及其作用,如何编写可测试的软件,以及 TDD 如何使我成为更好的开发者,以及如何让你受益。
通过网络直播可以观看有关 ES6 & React 的 TDD 内容
更多内容在 Lifetime Access Pass