jest 作为一款测试框架,拥有测试框架该有的一套体系,丰富的断言库,大多数api与老牌的测试框架如jasmine、mocha,譬如常用的expect、test(it)、toBe等,都非常好用。内部也是使用了jasmine作为基础,在其上封装。但是因为Snapshot这个特色功能,非常适合react项目的测试。
在项目根目录运行命令(mac) node --inspect-brk node_modules/.bin/jest --runInBand ,然后打开 chrome 浏览器,地址栏输入 chrome://inspect 点击 Open Dedicated DevTools for Node ,这时会自动打开一个调试面板,里面会在文件的最开始自动断点。
Note: the --runInBand cli option makes sure Jest runs the test in the same process rather than spawning processes for individual tests. Normally Jest parallelizes test runs across processes but it is hard to debug many processes at the same time.
# --inspect-brk 打开node调试模式
# --runInBand
# node --inspect-brk node_modules/.bin/jest --runInBand [any other arguments here]
# or on Windows
# node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand [any other arguments here]
会出现报错:Note: This is a precaution to guard against uninitialized mock variables. If it is ensured that the mock is required lazily, variable names prefixed with mock (case insensitive) are permitted.
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.
React单元测试实践
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。单元测试是由程序员自己来完成,最终受益的也是程序员自己。执行单元测试,就是为了尽量证明这段代码的行为和期望的一致。
其实我们每天都在做单元测试,包括那些认为自己从来没有写过单元测试的同事。你写了一个函数,log一下或者在界面上点一下,这,也是单元测试,把这种单元测试称为临时单元测试。临时单元测试的软件,一个是很难能够覆盖所有场景,二个是无法自动化运行,大幅度提高后期测试和维护成本。可以说,进行充分的单元测试,是提高软件质量,降低开发成本的必由之路。
下面讲一下我个人的 React 单元测试实践,旨在抛砖引玉。以下谈论到的优缺点以及实践方法等,均带有个人主观色彩,如有不同意见,望不吝赐教~
单元测试的优点
能够用更低的成本去验证代码的稳定性,基本保证目标代码在之后一直按照初始的预期运行,同时可以接入持续集成,进行低成本的重复使用,在第一时间发现问题,减少维护成本。
能够帮助开发者从另一个角度去思考如何组织代码,让代码结构更合理——你们有没有见过1000多行的类,方法里面全局状态内部属性随便使用,三个屏都看不完一个方法的那种。 主要体现在:能够促进思考方法、函数的边界,而不是不管三七二十一都放在一个方法里面;能够下意识的去写纯函数;
能够让你的同事觉得你有点东西🐶、
什么时候开始写单元测试
建议大家现在就开始尝试写单元测试。单元测试应该是对代码无侵入的,有没有单元测试都不影响你的代码功能实现。也就是说你完全可以有多少时间写多少测试,有写一个测试的时间就添加一个测试,没有时间就不写。
理由就是上面的第二点,只要你计划写单元测试,即使最后由于各种原因并没有写单元测试,你的代码也会和以前不同。相信我,我读书多不会骗你的🐶、
明确测试范围
在开始写单元测试之前,首先需要明确当前的测试是需要测试什么?
以组件为例,一般一个组件都会包含:ui呈现、工具方法、内部逻辑处理、第三方依赖、自己写的子组件。那么这么多东西哪些需要测试,哪些不需要测试,你在写测试之前就需要想好。
建议按照这样一个优先级顺序添加测试:关键逻辑代码、复杂逻辑代码、工具方法、其他代码。根据自己的时间,逐步提高测试覆盖率。
在实际操作上可以尝试在自测的环节,将盲目的界面自测操作,转变为单元测试代码,这样甚至可以用更少的时间得到更好的自测效果。
框架选型
测试组件库有很多,这里选用了目前最流行的: jest + enzyme (部分示例使用了 @testing-library/*),同时为了测试 hook 还使用了@testing-library/react-hooks
jest 作为一款测试框架,拥有测试框架该有的一套体系,丰富的断言库,大多数api与老牌的测试框架如jasmine、mocha,譬如常用的expect、test(it)、toBe等,都非常好用。内部也是使用了jasmine作为基础,在其上封装。但是因为Snapshot这个特色功能,非常适合react项目的测试。
enzyme 提供了几种方式将react组件渲染成真实的dom,提供了类似jquery的api来获取dom;提供了simulate函数来模拟事件触发;提供接口让我们获取到组件的state和props并且能对其进行操作。enzyme其实是react-test-renderer的封装,react-test-renderer的api非常不友好,但是enzyme开发的api跟jquery一致
@testing-library/react-hooks是一个专门用来测试React hook的库。我们知道虽然hook是一个函数,可是我们却不能用测试普通函数的方法来测试它们,因为它们的实际运行会涉及到很多React运行时(runtime)的东西,因此很多人为了测试自己的hook会编写一些
TestComponent
来运行它们,这种方法十分不方便而且很难覆盖到所有的情景。关于使用的框架建议:
常用操作
调试测试代码
测试代码也是代码,经常出现测试代码有误导致报错,一次次进行 log 输出效率低下,排查起来问题也是十分考验想象力,所以学习一下测试代码的调试也是很有必要的。 调试测试代码的原理就是利用 node 执行时 传入 --inspect-brk 参数进行调试,因为测试代码的运行是基于 node 环境的,所以就有如下调试方式:在运行测试命令时直接添加 --inspect-brk 参数结合 chrome 浏览器、利用编辑器的调试界面。
直接使用 --inspect-brk 参数运行测试脚本
在项目根目录运行命令(mac)
node --inspect-brk node_modules/.bin/jest --runInBand
,然后打开 chrome 浏览器,地址栏输入chrome://inspect
点击Open Dedicated DevTools for Node
,这时会自动打开一个调试面板,里面会在文件的最开始自动断点。使用编辑器的调试界面(推荐)
这里以 vscode 的操作步骤举例(下面的操作按钮均为默认窗口界面位置,如果自己以前自定义过请按自己自定义之后的实际菜单操作)
launch.json
,里面可能已经有了配置项,也可能没有记录快照
快照测试的优缺点:
我认为快照测试是利大于弊的,建议对基础场景进行快照记录,然后针对关键逻辑写普通测试
记录快照方法:
@testing-library/react
中的render
方法,此方法会深度遍历依赖,渲染得到完整的 dom 结构 —— 如果没有针对指定组件进行 mock 的话。jsdom
模拟。// 由于被测试组件中使用了 Link 组件 所以必须要用 Router 组件包裹 // 如果不想引入 Router 组件,则可以 mock 一下 Link 组件为 div 或者 a 标签啥的 import { MemoryRouter } from 'react-router-dom';
describe('snapshot', () => { it('library render', () => { // 渲染 const { container } = render(
}); });
触发事件
有时候需要测试页面的响应操作正不正常,这个时候就需要触发事件了,触发事件的方法为 fireEvent 或者 simulate
常用的 mock 方式
mock 一个依赖库中的某一个方法
场景:有个依赖库,需要 mock 这个库提供的一个方法,但是又担心只mock这个方法导致其他地方报错,这个时候就可以选择保留其他方法的同时mock这一个方法 jest.requireActual(moduleName)
mock 注意事项
mock
(case insensitive) are permitted.mock
(不区分大小写) 作为前缀。antd 3.X 版本的 form 属性 mock
3.x 版本的 antd ,经过 Form.create 包装的组件将会自带 this.props.form 属性,但是这样就给我们的测试带来了困难。 如果我们使用 enzyme 的 mount 方法进行渲染,而且只是进行逻辑的测试,那么问题不大。 因为我们可以直接拿到包装后的组件进行全量渲染,基本可以满足测试要求。 但是如果使用这样的渲染结果来记录快照是不太行的,输出的快照体积太大了,此时就需要使用 shallow 来进行渲染了。 然而使用 shallow 进行渲染并获取快照,就出现问题了,如果用包装后的组件来进行渲染,快照拿不到表单项的渲染结果,这样的快照是没有什么用的。 此时就会想到,能不能直接 shallow 渲染原组件,然鹅这样会收获一个报错
TypeError: Cannot destructure property
getFieldDecoratorof 'undefined' or 'null'.
, 因为你的组件拿不到 form 属性,太难了。。。此时坑已经挖了,我当然要负责任的填上,这里提供一种 hack 方法(不确定有没有更好的办法,如果有人知道的话请联系我)。
我们首先捋一下,我们现在的核心需求是:记录快照的时候需要把表单项记录下来,而且快照大小不能太大。 也就是说要求:首先必须使用 shallow 进行渲染;然后使用 shallow 就要求只能直接渲染原表单组件。 那么需要解决的问题就是:如果 mock 表单组件需要的 form 属性?
这里提供的思路就是:使用 Form.create 包装一个mock出来的组件,然后渲染这个包装后的组件,在渲染的过程中将拿到的 form 赋值给一个外界的变量, 然后这个变量就可以给我们需要测试的组件用了。
一些单元测试示例代码
测试工具函数、常量
快照测试
测试界面ui
测试组件逻辑
测试 hook
一些遇到的问题解决记录
react-i18next:: You will need to pass in an i18next instance by using initReactI18next
原因:被测试组件使用了 i18n 但是当前测试环境并没有初始化 i18n 的配置 解决:1、直接引入项目的 i18n 初始化配置(又会有 suspense 问题);2、mock 相关的多语言属性(推荐);
Login suspended while rendering, but no fallback UI was specified.
原因:出现这个报错,一个可能的原因是测试组件使用了
const { t } = useTranslation();
,而这个hook要求组件包裹在Suspense
组件中解决:
修改 i18n 的配置
给被测试组件包裹
Suspense
suspense 只渲染出了 fallback
原因:suspense 包裹的组件存在一个loading状态,一开始只会渲染出 fallback
Invariant failed: You should not use <Link> outside a <Router>
原因:被测试组件内部使用了
Link
组件,所以需要使用Router
组件进行包裹 解决:使用 BrowserRouter 作为包裹组件TypeError: window.matchMedia is not a function
Error: Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package)
原因:缺乏 canvas 功能的代码 解决:安装 jest-canvas-mock ,然后可以在当前测试文件引入或者在 setupTests.js 里面引入
TypeError: Cannot set property 'font' of undefined
上一步引入
jest-canvas-mock
后,还是会报这个错,可能是版本问题,网上大部分人都没有这个问题将
jest-canvas-mock
删除后重新安装 canvas 解决,一定要把前一个依赖删除干净。TypeError: Function.prototype.name sham getter called on non-function
这个报错只在生成测试覆盖率时出现,也就是使用了 --coverage 参数
原因:enzyme 遇到匿名类会出现问题
解决:给匿名导出的类添加类名
组件中有随机数,导致快照测试总是不通过
原因:随机数在每次生成快照都会不相同 解决: mock 那个随机数生成方法。例如:如果是使用 Math.random() 生成的随机数,就mock Math.random 方法;
比较常见的还有 new Date() 的返回,这个可以用 mockdate 库来 mock
如果那个随机数在外部依赖项中,可以找到依赖项的源码查看是什么随机方法,或者使用浅渲染,尝试规避掉那个随机数的渲染。
没有浅渲染方法
原因:@testing-library/react 没有提供浅渲染方法,如果需要浅渲染,需要使用 enzyme 。目前建议直接使用 enzyme。
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.
原因:使用了 enzyme 的 render 方法进行渲染HTML结构,此时 useLayoutEffect 会有提示,实际上渲染是成功了的,但是还是建议将这个报错处理掉。 解决:mock 整个 react 或者 只 mock useLayoutEffect hook 钩子。
Warning: An update to List inside a test was not wrapped in act(...)
@testing-library/react
框架是不需要手动去包裹 act 方法的using-act-and-wrapperupdate@testing-library/react
框架,还是出现了这个报错,那你也可以尝试再加一层 act 试试,如果问题解决,那么恭喜你可以跳过下面的步骤了react@17.0.1
的最新 api 就是使用 findBy* 选择器查找元素,这个解决办法也已经有人在上面的那个问答里面回复了。高阶组件返回的结果如何测试原始组件
SlotOperation
组件,因为这个组件使用了低版本的 antd 的 Form,必须要使用 Form.create 处理。这时就可以使用 enzyme 的 find 方法,根据组件名直接查找到需要测试的组件。