在React项目里保证组件质量,离不开一套顺手的测试组合。Jest是Facebook推出的测试框架,自带断言、Mock和覆盖率统计;Enzyme是Airbnb开源的React测试工具,能让我们像操作DOM一样去查看和交互组件。两者配合,基本覆盖了从函数到视图的验证需求。

一、环境准备与基础配置
新建一个基于React的项目后,通常可以用官方脚手架直接带上Jest。如果是在已有项目中补充,需要通过包管理器安装相关依赖。Jest默认会查找以test.js或spec.js结尾的文件,也支持在__tests__目录中放置用例。
Enzyme本身不包含适配器,必须安装对应React版本的enzyme-adapter-react-16或更高版本,并在测试入口处进行配置。下面是一段常见的setup文件内容,它告诉Enzyme用哪个适配器来解析组件。
// setupTests.js
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
配置完成后,在package.json里加上执行脚本,就可以用一条命令跑起全部测试。建议把setup文件通过Jest的setupFilesAfterEnv选项注册,这样每个用例运行前都会自动加载适配器。
很多初学者会忽略适配器版本与React版本的匹配,导致报错提示无法找到React实例。遇到这类问题,先核对React主版本,再选择对应的adapter包,基本都能解决。
二、shallow与mount该怎么选
Enzyme提供三种渲染方式,实际写单测最常用的是shallow和mount。shallow只渲染当前组件本身,不深入子组件内部;mount则会完整渲染整棵组件树,包含子组件的真实输出。
当你只想验证当前组件是否正确地把props传给了子组件,或者当前组件的状态逻辑是否独立正确,shallow更快也更隔离。比如父组件里写了一个<Child title={text} />,用shallow能直接查到这个Child节点的props,而不用关心Child内部如何实现。
// 使用shallow查找子组件props
import { shallow } from 'enzyme';
import Parent from './Parent';
test('父组件传递title给子组件', () => {
const wrapper = shallow(<Parent text="你好" />);
expect(wrapper.find('Child').prop('title')).toBe('你好');
});
mount适合需要测试生命周期、ref或者子组件交互联动的场景。例如一个列表组件点击添加按钮后,子项的条数真实增加,这种情况用mount才能看到完整的DOM变化。代价是执行更慢,且容易受子组件bug影响。
实践里常见做法是:纯逻辑和props透传用shallow,涉及真实DOM事件、第三方组件集成用mount。这样既能保持用例稳定,也不会牺牲关键路径的覆盖率。
三、模拟用户交互与断言
单元测试的核心之一是模拟用户行为。Enzyme的wrapper对象提供了simulate方法,可以触发点击、输入等事件。配合Jest的expect,就能完成“操作—结果”的验证闭环。
下面例子里,组件有一个按钮,点击后计数加一。我们用shallow渲染,通过simulate('click')模拟点击,再去读组件state确认数值变化。注意Enzyme的state()方法返回的是当前内部状态副本。
// Counter.jsx
import React from 'react';
export default class Counter extends React.Component {
state = { count: 0 };
increment = () => this.setState({ count: this.state.count + 1 });
render() {
return <button onClick={this.increment}>点击{this.state.count}</button>;
}
}
// Counter.test.js
import { shallow } from 'enzyme';
import Counter from './Counter';
test('点击按钮计数加一', () => {
const wrapper = shallow(<Counter />);
expect(wrapper.state('count')).toBe(0);
wrapper.find('button').simulate('click');
expect(wrapper.state('count')).toBe(1);
});
如果组件使用了函数式写法配合Hooks,可以用React Testing Library思路,也可以继续用Enzyme的mount配合act。不过Enzyme对Hooks的支持不如对类组件自然,复杂Hook逻辑建议额外补充Jest纯函数测试。
断言方面,Jest内置了toBe、toEqual、toContain等匹配器,基本不用引第三方库。当需要验证函数是否被调用,可以用jest.fn()生成Mock函数,再用toHaveBeenCalled检查调用情况。
四、快照测试与维护
Jest的快照功能可以记录组件某次渲染的输出,后续运行若结构变化就会报错。它适合防止UI被无意篡改。第一次运行会生成__snapshots__目录,里面是序列化后的结构文本。
使用Enzyme的mount配合Jest快照,可以这样写:先渲染组件,再调用expect(wrapper).toMatchSnapshot()。一旦产品经理调整了类名或结构,快照失败,你需要人工确认是预期改动还是bug,确认后执行更新命令重写快照。
import { mount } from 'enzyme';
import Card from './Card';
test('Card组件快照', () => {
const wrapper = mount(<Card title="标题" />);
expect(wrapper).toMatchSnapshot();
});
快照不是万能的,它只比对渲染结构,不验证交互逻辑。如果团队频繁改版,快照反而会变成噪音。建议只对稳定组件或基础UI库开启,业务页面试图用具体断言代替。
当快照失败且确属正常迭代,使用jest -u更新即可。但要在代码评审时附带说明,避免有人顺手更新掩盖了真实回归问题。
五、常见误区与建议
一个典型误区是测试用例过度依赖真实网络。Jest提供了jest.mock来拦截模块,你应该把接口请求Mock掉,只验证组件在拿到数据后的表现,而不是真的发请求。
另一个问题是把太多逻辑写进用例里,导致测试比组件还难维护。好的单测应该短小、只测一个点。可以用beforeEach统一构造基础wrapper,把重复代码收拢起来。
beforeEach(() => {
wrapper = shallow(<Form initial="test" />);
});
test('初始值正确', () => {
expect(wrapper.state('value')).toBe('test');
});
最后,覆盖率不是目的而是手段。Jest跑完会输出覆盖率报告,重点看核心计算和关键交互是否覆盖,不必强求百分之百。把单测当作改代码前的保护网,而不是交付负担,团队才会真正用起来。
把Jest与Enzyme纳入日常开发,最好从第一个组件就写用例,而不是等代码堆积后再补。这样成本最低,也能让后续接手的人放心重构。
JestEnzymeReact_unit_test修改时间:2026-08-09 05:15:33