使用 Jest 测试 React 应用:从环境搭建、快照测试到 DOM 交互的完整实战指南
【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest
Jest 本身就是 Facebook(Meta)在测试 React 应用的过程中诞生并持续打磨的测试框架,本指南围绕 Jest 29 中《Testing React Apps》的完整脉络,讲解如何在 React 项目中配置 Jest(含 Create React App 与手动搭建两条路径)、用react-test-renderer编写快照测试、处理 Enzyme 与 React 16+ 的兼容警告,以及用@testing-library/react做 DOM 交互断言,最后深入自定义 transformer 的编写。读完本文,你将获得一套可直接复制、可运行、有仓库源码佐证的 React 测试方案。
Setup:搭建 React 项目的 Jest 测试环境
使用 Create React App(推荐新手)
如果你刚开始接触 React,官方推荐直接使用 Create React App。它开箱即用,且自带 Jest——创建项目后无需任何额外 Jest 配置即可运行测试。你唯一需要补充安装的是react-test-renderer,用于渲染组件并生成快照:
npm install --save-dev react-test-renderer不依赖 Create React App 手动搭建
如果你在已有应用(或自定义脚手架)中接入 Jest,则需要安装一组包,让 Babel 转换与测试环境协同工作。核心思路是:用babel-jest配合 React 的 Babel preset,在测试环境中把 JSX / 新版 JavaScript 语法转换为 Jest 可执行的代码(详见 使用 Babel):
npm install --save-dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer安装完成后,package.json大致如下(<current-version>表示对应包的当前实际版本,请以你安装到的版本为准)。请务必补上scripts.test与 jest 相关配置项:
{ "dependencies": { "react": "<current-version>", "react-dom": "<current-version>" }, "devDependencies": { "@babel/preset-env": "<current-version>", "@babel/preset-react": "<current-version>", "babel-jest": "<current-version>", "jest": "<current-version>", "react-test-renderer": "<current-version>" }, "scripts": { "test": "jest" } }并在项目根目录创建 Babel 配置文件:
module.exports = { presets: [ '@babel/preset-env', ['@babel/preset-react', {runtime: 'automatic'}], ], };这里runtime: 'automatic'表示使用 React 17+ 的 JSX 转换方式,无需在文件顶部显式import React。做完以上两步,就可以开始写测试了。
Snapshot Testing:用 react-test-renderer 捕获组件渲染结果
编写一个 Link 组件的快照测试
以渲染超链接的Link组件为例(完整示例见 examples/snapshot/Link.js),它根据鼠标悬停状态切换className:
import {useState} from 'react'; const STATUS = { HOVERED: 'hovered', NORMAL: 'normal', }; export default function Link({page, children}) { const [status, setStatus] = useState(STATUS.NORMAL); const onMouseEnter = () => { setStatus(STATUS.HOVERED); }; const onMouseLeave = () => { setStatus(STATUS.NORMAL); }; return ( <a className={status} href={page || '#'} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} > {children} </a> ); }说明:示例使用函数组件,但类组件可以用完全相同的方式测试(参见 React 官方文档 Function and Class Components)。需要提醒的是,对于类组件,Jest 期望测试的是props(对外行为)而非直接调用内部方法。
接下来,用 React 的react-test-renderer渲染组件、通过toMatchSnapshot()捕获渲染输出,并借助renderer.act()手动触发回调,验证状态切换前后的三种渲染形态:
import renderer from 'react-test-renderer'; import Link from '../Link'; it('changes the class when hovered', () => { const component = renderer.create( <Link page="http://www.facebook.com">Facebook</Link>, ); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); // manually trigger the callback renderer.act(() => { tree.props.onMouseEnter(); }); // re-rendering tree = component.toJSON(); expect(tree).toMatchSnapshot(); // manually trigger the callback renderer.act(() => { tree.props.onMouseLeave(); }); // re-rendering tree = component.toJSON(); expect(tree).toMatchSnapshot(); });生成的快照文件长什么样
运行yarn test或jest后,会在__tests__/__snapshots__/目录下生成与测试文件同名的.snap文件(仓库中的真实产物见 examples/snapshot/tests/snapshots/link.test.js.snap),内容形如:
exports[`changes the class when hovered 1`] = ` <a className="normal" href="http://www.facebook.com" onMouseEnter={[Function]} onMouseLeave={[Function]} > Facebook </a> `; exports[`changes the class when hovered 2`] = ` <a className="hovered" href="http://www.facebook.com" onMouseEnter={[Function]} onMouseLeave={[Function]} > Facebook </a> `; exports[`changes the class when hovered 3`] = ` <a className="normal" href="http://www.facebook.com" onMouseEnter={[Function]} onMouseLeave={[Function]} > Facebook </a> `;每个exports[...]块对应测试中一次toMatchSnapshot()调用:初始状态className="normal"、悬停后className="hovered"、离开后又回到normal,完整记录了组件状态机随交互变化的渲染结果。
快照的更新与提交
快照文件应当与代码变更一起提交到版本库。下次运行测试时,渲染输出会与已保存的快照逐字对比;一旦不匹配,测试即失败。此时需要人工判断:这是预期的变更(例如 UI 调整)还是意外回归。若确认是预期变更,运行jest -u(即--updateSnapshot)覆盖既有快照。
该示例的完整可运行代码位于仓库的 examples/snapshot 目录,其中
package.json声明了jest配置"testEnvironment": "jsdom",与下面 DOM 测试的场景相互配合。
深入:快照断言与序列化背后的机制
快照测试并非魔术。toMatchSnapshot()在 packages/jest-snapshot 包中实现:首次执行时把序列化后的值写入.snap文件(文件头会标注// Jest Snapshot v1),之后每次执行都会反序列化旧快照并与新值比对。在 React 场景中,react-test-renderer的toJSON()负责把组件树折叠为纯 JSON 结构(标签名、props、子节点),因此快照文件里能看到完整的className、href与函数引用占位符。理解这一点,你就知道为什么“快照对比的是渲染结果而非组件内部状态”。
Snapshot Testing with Mocks, Enzyme and React 16+
在 Enzyme + React 16+ 的组合下做快照测试有一个知名坑:如果按下面这种“字符串短路”的方式 mock 组件:
jest.mock('../SomeDirectory/SomeComponent', () => 'SomeComponent');控制台会出现警告:
Warning: <SomeComponent /> is using uppercase HTML. Always use lowercase HTML tags in React. # Or: Warning: The tag <SomeComponent> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.原因是 React 16 对元素类型有严格的校验逻辑,而上述 mock 返回的字符串无法通过该校验。有四种解决方案,按推荐程度排序:
渲染为纯文本:简单直接,但快照中看不到传给 mock 组件的 props:
jest.mock('./SomeComponent', () => () => 'SomeComponent');渲染为自定义元素:DOM “自定义元素”(custom elements)不参与任何类型校验、不会触发警告。其规范要求小写且名称含连字符:
jest.mock('./Widget', () => () => <mock-widget />);改用
react-test-renderer:测试渲染器不关心元素类型,可以愉快地接受如SomeComponent这种大写标签。实践中可以:用测试渲染器做快照断言、用 Enzyme 单独验证组件行为。整体禁用警告(在 jest setup 文件中配置):
jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction'));这通常不应成为首选,因为会连带丢失真正有价值的警告。但在某些场景下是合理的,例如测试 react-native 组件时把 react-native 标签渲染进了 DOM,大量警告本身无意义。另一种思路是替换
console.warn,仅屏蔽特定警告。
DOM Testing:断言并操纵已渲染的组件
如果需要在真实 DOM 环境中做断言并操纵渲染结果,可以使用 @testing-library/react、Enzyme 或 React 官方的 TestUtils。下面以@testing-library/react为例。
安装:
npm install --save-dev @testing-library/react注意:
@testing-library/react@9.0.0及以上版本会自动在afterEach中执行 cleanup(卸载组件、清理 DOM),因此下文示例中的afterEach(cleanup)在实际高版本中可以省略——仓库示例 examples/react-testing-library/tests/CheckboxWithLabel-test.js 就没有显式 cleanup。
先实现一个在两个标签文案间切换的复选框组件:
import {useState} from 'react'; export default function CheckboxWithLabel({labelOn, labelOff}) { const [isChecked, setIsChecked] = useState(false); const onChange = () => { setIsChecked(!isChecked); }; return ( <label> <input type="checkbox" checked={isChecked} onChange={onChange} /> {isChecked ? labelOn : labelOff} </label> ); }对应测试(完整示例见 examples/react-testing-library,其package.json使用testEnvironment: "jsdom"):
import {cleanup, fireEvent, render} from '@testing-library/react'; import CheckboxWithLabel from '../CheckboxWithLabel'; // Note: running cleanup afterEach is done automatically for you in @testing-library/react@9.0.0 or higher // unmount and cleanup DOM after the test is finished. afterEach(cleanup); it('CheckboxWithLabel changes the text after click', () => { const {queryByLabelText, getByLabelText} = render( <CheckboxWithLabel labelOn="On" labelOff="Off" />, ); expect(queryByLabelText(/off/i)).toBeTruthy(); fireEvent.click(getByLabelText(/off/i)); expect(queryByLabelText(/on/i)).toBeTruthy(); });这个测试完整演示了 DOM 测试的典型链路:
render():把组件挂载进 jsdom 模拟的 DOM 中,返回queryByLabelText、getByLabelText等查询方法;- 断言初始态:
queryByLabelText(/off/i)用正则按 label 文本查询元素,验证初始渲染的是 "Off"; fireEvent.click():模拟用户点击(由于<input>包裹在<label>中,点击 label 会命中复选框);- 断言交互结果:点击后状态翻转,查询
on文本成功,证明onChange事件处理生效。
仓库中 examples/snapshot/tests/link.test.js 还展示了更贴近真实用户的写法:用@testing-library/user-event的userEvent.hover()/userEvent.unhover()触发悬停,配合screen.findByLabelText()异步断言 class 变化,可以与本指南的react-test-renderer方案对照学习。
Custom Transformers:定制代码转换逻辑
如果你的项目需要更高级的转换能力,可以不使用babel-jest,而自己编写 transformer。下面的例子基于@babel/core实现:对每个源文件调用 Babel 的transform,并混入babel-preset-jest(该 preset 负责注入 Jest 所需的 Babel 插件,如 hoistjest.mock调用):
'use strict'; const {transform} = require('@babel/core'); const jestPreset = require('babel-preset-jest'); module.exports = { process(src, filename) { const result = transform(src, { filename, presets: [jestPreset], }); return result || src; }, };别忘了为这个示例安装@babel/core和babel-preset-jest两个包。然后在 Jest 配置中注册该 transformer,将.js文件指向它:
"transform": {"\\.js$": "path/to/custom-transformer.js"}基于 babel-jest 组合自定义 transformer
如果你需要 Babel 支持、又想要自定义配置,更优雅的做法是直接基于babel-jest的createTransformer组合:它返回一个符合 Jest transformer 接口的对象,你只需传入自己的 Babel 选项:
const babelJest = require('babel-jest'); module.exports = babelJest.createTransformer({ presets: ['my-custom-preset'], });从源码看,createTransformer在 packages/babel-jest/src/index.ts 中实现,有几个值得了解的细节:
- 自动注入 jest preset:
createTransformer会把babel-preset-jest追加到presets末尾(除非显式传入excludeJestPreset: true),同时设置caller: {name: 'babel-jest', ...}让 Babel 感知调用方能力; - 缓存键(cache key):
getCacheKey基于源码文本、Babel 配置、相对路径、NODE_ENV、BABEL_ENV与 Node 版本等信息计算 SHA-1 哈希(取前 32 位),保证配置或环境变化时缓存自动失效; - 覆盖率插桩:当
transformOptions.instrument为真(即开启 coverage)时,会自动注入babel-plugin-istanbul实现源码级覆盖率,无需手工配置; - source map:转换输出默认携带
sourceMaps: 'both',便于堆栈溯源。
想深入 transformer 的完整接口(process/getCacheKey/canInstrument等),可参考 CodeTransformation.md 的专项文档。
小结
围绕 Jest 29 的 React 测试指南,本文覆盖了从零到实战的完整路径:搭建阶段可选择 Create React App 零配置接入或手动配置babel-jest+ React preset;快照测试用react-test-renderer捕获渲染输出并用jest -u管理快照更新;DOM 测试推荐@testing-library/react以用户视角断言交互;Enzyme + React 16+场景下则需按四种方案规避 mock 警告;定制化需求可通过自定义 transformer 或组合babel-jest.createTransformer实现。上述所有示例均可在仓库 examples/snapshot 与 examples/react-testing-library 中找到可运行源码,作为你搭建 React 测试体系时的直接参照。
【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考