四、单元测试
Jest
在 JavaScript 的世界里,单元测试的框架很多,品牌最老名气最响的是 Mocha ,不过,不要纠结于名气,请使用 Jest 。你不会后悔的,接下来我告诉你为什么。
我们先假设,作为开发者,你是在团队中工作。所谓团队,就是有很多人一起工作,而且随着业务和团队的发展,人会越来越多,潜台词就是——不确定因素越来越多。
人和人之间交流会出现偏差,人的水平有高低之分,人也会犯错,总之,你不能指望所有人都把事情做得尽善尽美。
具体到单元测试这件事上来,测试驱动
是开发喊了这么多年,为什么真正做到这一点的团队依然不多呢?因为,当团队变大之后,很多问题也就出现了
1、单元测试用例庞大,执行时间过长。
想象一下,一个代码库里假设有一千个单元测试用例,即使每个单元测试用例平均只需要 10 毫秒,那总时间也就需要 10 秒钟。好,假设代码库进一步扩大,有了一万个单元测试用例,那就跑一遍就需要 100 秒,已经超过了一分钟。这还只是保守估计,实际上单元测试用例的运行时间只会比这长。开发者如果每次修改都需要等待这么漫长的单元测试运行时间,肯定会三心二意上网去看其他东西。
2、 单元测试用例之间相互影响。
你可能也有这样的体验,代码库中的单元测试突然失败了,但是你修改的代码根本不会取影响失败的那个单元测试用例,怎么回事?这往往是因为某个成员以前的代码写得不好,影响了一个全局变量。当然,谁都知道单元测试应该在 setup 时创建环境,在 teardown 时恢复环境,可是,总会有人有马虎大意的情况,这时候你怎么办?要么你只好去修复一个本不是你改坏的代码,要么你干脆删掉那段不可靠的单元测试代码,不管怎样,这都会打击你支持测试驱动开发
的决心。
Jest 较好地解决了上面说的问题,因为 Jest 最重要的一个特性,就是支持并行执行
Mocha 之类老牌单元测试框架,把所有的单元测试都放在一个环境中执行,这就使所有单元测试访问的是同样一个全局变量空间,所以只要测试代码没写好,就会互相影响。而且,为了保证执行正常,所有的单元测试必须一个接一个地执行,这是体系架构决定的,没有办法。
Jest 不同,Jest 为每一个单元测试文件创造一个独立的运行环境,换句话说,Jest 会启动一个进程执行一个单元测试文件,运行结束之后,就把这个执行进程废弃了,这个单元测试文件即使写得比较差,把全局变量污染得一团糟,也不会影响其他单元测试文件,因为其他单元测试文件是用另一个进程来执行
更妙的是,因为每个单元测试文件之间再无纠葛,Jest 可以启动多个进程同时运行不同的文件,这样就充分利用了电脑的多 CPU 多核,单进程 100 秒才完成的测试执行过程,8 核只需要 12.5 秒,速度快了很多。
Jest 还有很多其他友好的特性,大家可以自己去发掘,这里废话不多说,只想安利各位,测试 React 或者 JavaScript 代码,用 Jest!
使用 create-react-app 产生的项目自带 Jest 作为测试框架,不奇怪,因为 Jest 和 React 一样都是出自 Facebook。
运行下面的命令,就可以进入交互式的测试驱动开发模式:
npm test
Enzyme
虽然最好的 React 测试框架出自 Facebook 家,最受欢迎的 React 测试工具库却出自 Airbnb,这个工具库叫做 Enzyme。Enzyme 这个单词的含义是酶
,至于命名原因已经无法考据,可能寓意着快速分解。
不过因为 Enzyme 不是 Facebook 家出品,所以使用 Enzyme 还真稍微有些麻烦——在 create-react-app 产生的应用中并不包含 Enzyme,需要我们自己来添加。
在项目目录下,通过下面的命令来安装 enzyme
npm i --save-dev enzyme enzyme-adapter-react-16
可以注意到,我们不光要安装 enzyme,还要安装 enzyme-adapter-react-16,这个库是用来作为适配器的。因为不同 React 版本有各自特点,所用的适配器也会不同,我们的项目中使用的是 16.4 之后的版本,所以用 enzyme-adapter-react-16;如果用 16.3 版本,需要用 enzyme-adapter-react-16.3;如果用 16.2 版本,需要用 enzyme-adapter-react-16.2;如果用更老的版本 15.5,需要用 enzyme-adapter-react-15。具体各个 React 版本对应什么样的 Adapter,请参考 enzyme 官方文档。
现在,可以在测试代码中使用 enzyme 了。我们以之前秒表应用中的 ControlButtons 组件为例,来说明如何做单元测试。
我们创造一个 ControlButtons.test.js,来容纳对应的测试用例,因为所有后缀为 .test.js 的文件都会被 Jest 认作是测试用例文件。
在代码中,需要使用 Adapter,代码如下
import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
我们对 ControlButtons 组件的测试,就是要渲染它一次,看看渲染结果如何,enzyme 就能帮助我们做这件事。
比如,我们想要保证渲染出来的内容必须包含两个按钮,其中一个按钮的 class 名是 left-btn,另一个是 right-btn,那么我们就需要下面的单元测试用例:
import {shallow} from 'enzyme';
it('renders without crashing', () => {
const wrapper = shallow(<ControlButtons />);
expect(wrapper.find('.left-btn')).toHaveLength(1);
expect(wrapper.find('.right-btn')).toHaveLength(1);
});
在这里我们使用了 shallow,其实也可以使用 mount。
shallow 和 mount 的区别,就是 shallow 只会渲染被测试的 React 组件这一层,不会渲染子组件;而 mount 则是完整地渲染 React 组件包括其所有子组件,包括触发 componentDidMount 生命周期函数。
原则上,能用 shallow 就尽量用 shallow,首先是为了测试性能考虑,其次是可以减少组件之间的影响,比如,一个组件 Foo 有子组件 Bar,如下
const Foo = () => ()
<div>
{/* other logic */
<Bar />
</div>
)
如果用 mount 去渲染 Foo,会连带 Bar 一起完全渲染,如果 Bar 出了什么毛病,那 Foo 的单元测试也过不了;如果用 shallow,只知道 Bar 曾经被用,即使 Bar 哪里出了问题,也不影响 Foo 的单元测试。
这并不是说我们就不管 Bar,Bar 的质量会由它自己的单元测试来检验,这就引出下一个话题——代码覆盖率。
代码覆盖率
你不能给自己的程序随便写几个单元测试,就说自己的代码已经测试好了,就像上面我只给 ControlButtons 组件写了一个测试用例,我并不能说整个秒表应用已经通过了测试。
你的代码测试覆盖率只有达到一定程度,才好说自己的代码已经被测试了。
剩下来就是一个纠结的问题:代码测试的覆盖率应该达到多少才算够?
以我个人的经验,代码覆盖率必须达到 100%,也就是说,一个应用不光所有的单元测试都要通过,而且所有单元测试都必须覆盖到代码 100% 的角落。
如果对覆盖率的要求低于 100%,时间一长,质量必定会越来越下滑。
遇到一个不好测试的代码,开发者倾向于不去考虑如何重构代码提高可测试性,而是直接忽略这部分代码不去测试,反正不要求 100% 嘛;遇到工期比较紧的时候,甚至会进一步降低代码覆盖率要求,用牺牲质量来加快开发速度,反正不要求 100% 嘛。
所以,如果你真的对代码质量认真负责的话,请坚守 100% 代码覆盖率的底线!
在 create-react-app 创造的应用中,已经自带了代码覆盖率的支持,运行下面的命令,不光会运行所有单元测试,也会得到覆盖率汇报
npm test -- --coverage
代码覆盖率包含四个方面:
- 语句覆盖率
- 逻辑分支覆盖率
- 函数覆盖率
- 代码行覆盖率
只有四个方面都是 100%,才算真的 100%。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论