Jest+enzyme,写 React 单测有点香也有点坑

3,102 阅读8分钟

前言

最近在给项目代码补单测,React 项目,第一反应就是成熟的解决方案 Jest + enzyme。按说读读 API,三两下应该就能写好,然而过程中却有点磕碰,遇到一些始料未及的问题。有些是关于 enzyme 的使用方法问题,有些则是对单测思路的理解不够。现在整理出来给大家参考哈~

准备

这里没啥好说,通过 github 指引安装好 Jest 和 enzyme 并做好配置即可:

Jest: github.com/facebook/je…

enzyme: github.com/enzymejs/en…

记得,根据 enzyme 的指引在 Jest 的配置里做统一配置(github.com/enzymejs/en…),就不需要每个单测文件去写应用 Adapter 的代码了。

好用的 VSCode 插件

VSCode 有很多好用的插件,做 Jest 单测可以装 Jest Runner 插件,随时执行用例,只执行某个 describe 甚至某个 it,非常方便,甚至可以逐步 debug:

如果你的 Jest 的配置文件不是根目录的 jest.config.js ,则要在插件设置里修改:

建议把覆盖率开关给关掉(毕竟你有很多单测文件单测用例,没法使用同一个单测覆盖配置吧)。

最实用的 API:debug

Jest + enzyme 入手简单,看看官方 demo 就知道怎么写,各个 API 看官方文档就可以了。但是在你开始写单测之前,郑重推荐 enzyme 的 debug API。使用方法:

const component = shallow(<Test />);
console.log(component.debug());

配合 console.log,它可以将 enzyme 渲染出来的 React 组件的 DOM 结构,看下图就知道了。在你写单测报错百思不得其解的时候,把这个打印出来可以发现并解决大部分问题!实乃居家必备!

shallow 还是 mount

喜欢 enzyme 的一个原因就是它提供 shallow 渲染方式,也叫浅渲染。顾名思义:

mount:会对 React 组件进行完整渲染,包括每个子组件,最后生成的结构是我们熟悉的 DOM 结构的样子;

shallow: 只会渲染一层。碰到子组件就当做 web component 的节点看,原样保留。如上节图示。

做单测的一个原则,是要尽量保证被测对象的纯净性。 测一个文件就尽量不要引入其他文件,测一个函数就尽量不要涉及其他函数。同理,测一个 React 组件就尽量不要引入其他组件的逻辑,其他组件就应该在它自己的用例里测试。

所以,除了一些特殊情况,尽量只用 shallow。至于特殊情况,下文会提到。

mock、mock、mock!

上节提到要保证被测对象纯净性,那要测文件 A 确实需要 import 文件 BCD 才能跑起来,怎么办?都 mock 掉就完事了。

Jest 提供了不少 mock 用的函数,最主要有:

jest.fn:对函数进行 mock,执行一个空函数,不执行原函数,返回 jest mock function。也可以传参替换成执行你传入的函数;

jest.spyOn:跟 jest.fn 差不多,只不过它会执行原函数,同返回 jest mock function;

jest.fn().mockImplementation:对带原型的函数进行 mock;

jest.mock:对模块进行 mock。

mock 完使用 toHaveBeenCalled、toHaveBeenCalledWith、toHaveBeenCalledTimes 来检测,用法见 官方文档 即可。然而在使用过程中,还有不少小技巧。

1. mock 模块有些小坑

首先,路径怎么写?jest.mock 第一个参数是要 mock 的包的路径,那这个路径是写相对于单测文件的路径,还是被测试文件里引用的路径?

经测试验证,这里的路径是相对于单测文件的,只要单测文件通过这个路径可以 import 到对应的文件即可。比如:

单测文件和被测文件的路径不必写成一样,只要文件系统能对应到同个文件即可。

接着,别在 describe 里面对模块进行 mock,没用,不生效。Jest 会将 jest.mock 语句提升到代码文件的最前面,也就没法在代码中间动态 mock 成不同的样子。

再有,使用 jest.mock 的第二个参数做模块自定义 mock 时,不能使用外部变量。如下图:

也同样没法从其他文件引入对象作为要 mock 的结果:

之所以这样估计也是因为 jest.mock 自动提升到代码文件最前面的特性。

2. 网络请求怎么 mock?

直接对做网络请求的包 mock 掉嘛,如 axios,然后同步返回编好的假数据。

这时有同学会问了,那我同个请求同样的参数调用,希望测到成功、失败等多种情况,怎么办??

使用点小技巧即可:

3. 我就是需要动态 mock 模块怎么办?

前文提到不要在 describe 里面使用 jest.mock ,那如果实际开发中确实需要对一个文件反复 mock 呢?比如要测试文件 A,里面用了文件 ua.ts,它会返回当前页面的相关 ua 信息。而单测里面需要不断切换 ua 做不同的测试用例。

此时有两种方案可以考虑:

方案1: hack 的小技巧,像上面 mock 网络请求一样,mock 这个模块的时候留一个自定义的接口如 setUa ,然后就可以在每个单测用例前调用切换参数。虽是 hack 且可能会稍微影响到 TS 但好用;

方案2:使用官方接口 jest.doMock,这个接口就不会自动提升到代码顶部,可以动态 mock,但使用时就要很注意:所有依赖于要 mock 文件的代码最好在 mock 之后才 import 进来,这样才能用上 mock 代码。改成动态 import 可参考官方文档

两者各有优劣,自行根据情况选择哈。

4. mock 模块统一管理

官方示例是在要 mock 的文件同级创建 __mocks__ 文件夹放同名文件,使用 jest.mock('xxx') 即可自动匹配。

但是我们的项目,希望单测的文件都统一放在 <rootDir>/test/unit 下面,单测的 mock 文件统一放在 <rootDir>/test/unit/__mocks__ 下面,怎办?Jest 提供了相关配置。

首先改 Jest 的配置,增加 roots:

接着在 __mocks__ 目录下根据 mock 时写的路径一层层建目录和文件即可。比如,使用时 jest.mock('aa/bb'),只要建立 __mocks_/aa/bb.ts 即可自动匹配替换。

这里要特别强调一个点:当我们对 npm 包做上述处理时,Jest 会自动替换:

也就是说不管你写不写 jest.mock('xxx') 这一句,只要 __mocks__ 目录下有对应文件,Jest 就会自动替换,所有用例文件及它们引入的被测文件都生效。一般情况下这样做也没什么问题,毕竟外部 npm 包一般比较稳定。但是如果我们项目中使用了 lerna 来管理代码,那就不一样了。

举个例子,A 同学要测试的文件用了项目的 lerna 包 @project/package1,他在 __mocks__ 目录放了个 mock 文件替换。此时 BCDEF 等等其他同学的用例可能就跑不起来了,因为他们测试的源文件引用了那个包,被自动替换成 mock 文件,而那个 mock 文件不符合他们场景的需要,于是 GG 了。即是说,lerna 包没法像项目的普通文件一样,通过写不写 jest.mock 由单测文件决定要不要 mock,而是被全自动 mock

谨慎对 lerna 包进行 mock

一些小 Tips

Tips1:ref 函数怎么触发

写 React 组件用到 ref 并不奇怪,特别是函数形式:

但是用 enzyme 测试的时候却发现 ref 函数没执行?!

执行 ref 函数,要用 enzyme 的 mount 方法来加载组件。这就是我前文提到的特殊情况。

Tips2:谨慎对待 setState

在 componentDidMount、事件触发等情况下调用 setState,这是很常见的。大家都知道 React 的 setState 方法是异步的,不过 enzyme 的不同模式对 setState 的处理不一样。

在 shallow 渲染模式下,调用 setState 会同步更新 state 的值:

但是如果用 mount 模式,组件内部调用 setState 会异步生效,此时要强制更新:

所以,尽量使用 shallow。

Tips3:用好纯函数组件

有这样一个例子:有一个函数根据传进来的参数不同,返回不同的 React 组件。现在对它做单测:

第一个用例成功了,第二个却失败了??通过 console.log(component.debug()) 发现按钮 B 的 Test 组件被展开了。不是说用 shallow 不会对子组件做展开吗?

其实 shallow 会展开第一层节点,如 shallow(<CompA />) 会展开 CompA 组件 ,而这里函数执行后第二个用例 Test 组件变为第一层组件被展开,于是没法像按钮 A 一样做测试了(子组件对 name 属性消化用在别的地方了)。

怎么改?在 Test 外面多包一层?更好的做法还是使用纯函数组件:

总结

总的来说 Jest + enzyme 确实好用,本文只是我的一些浅显经验,肯定有更多的技巧和好用的工具待解锁,欢迎大家留言交流哈~