如何用react-testing-library测试React函数组件的重渲染行为
测试实现思路
要实现防止React.memo性能回归的测试,核心逻辑是统计子组件的渲染次数,验证状态更新后仅目标子组件重渲染:
- 先固定模拟列表数据源,保证每次测试都有至少2个可对比的子组件
- 给子组件的渲染逻辑添加调用次数间谍
- 初始渲染父组件后清空计数,排除初始渲染的统计干扰
- 触发单个子组件的状态更新操作
- 验证未被操作的子组件渲染次数无变化,仅被操作的子组件触发重渲染
方案1:Jest + React Testing Library 实现
基础实现
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import Parent from './Parent'; import Child from './Child'; // 模拟数据源接口返回固定测试数据 jest.mock('./api', () => ({ queryItems: () => [ { id: 1, title: '测试项1' }, { id: 2, title: '测试项2' }, { id: 3, title: '测试项3' }, ] })); describe('列表渲染性能验证', () => { it('单个子项操作时其余子项不触发重渲染', async () => { // 给memo包裹的Child组件的渲染逻辑加间谍 const childRenderSpy = jest.spyOn(Child, 'type'); render(<Parent />); // 初始渲染3个子组件,调用3次渲染函数 expect(childRenderSpy).toHaveBeenCalledTimes(3); childRenderSpy.mockClear(); // 清空计数,仅统计更新阶段的渲染 // 触发第一个子项的点击操作 fireEvent.click(screen.getAllByRole('button')[0]); // 等待状态更新完成 await waitFor(() => { // 验证仅触发1次子组件渲染,且是被点击的id=1的子项 expect(childRenderSpy).toHaveBeenCalledTimes(1); expect(childRenderSpy).toHaveBeenLastCalledWith( expect.objectContaining({ item: expect.objectContaining({ id: 1 }) }), expect.anything() ); }) }); });
备选实现(spy不生效时使用)
如果因为组件导出方式问题导致上面的spy方法失效,可以直接mock子组件加入计数逻辑:
// 放在测试文件顶部,替换原有Child导入逻辑 let childRenderCount = 0; let lastRenderedItemId = null; jest.mock('./Child', () => { const originalModule = jest.requireActual('./Child'); return { __esModule: true, default: jest.fn((props) => { childRenderCount++; lastRenderedItemId = props.item.id; return originalModule.default(props); }) }; }); // 测试用例内直接通过变量统计渲染次数 it('验证重渲染逻辑', async () => { childRenderCount = 0; render(<Parent />); expect(childRenderCount).toBe(3); childRenderCount = 0; fireEvent.click(screen.getAllByRole('button')[0]); await waitFor(() => { expect(childRenderCount).toBe(1); expect(lastRenderedItemId).toBe(1); }) })
方案2:Jest + Enzyme 实现
import { mount } from 'enzyme'; import Parent from './Parent'; import Child from './Child'; jest.mock('./api', () => ({ queryItems: () => [ { id: 1, title: '测试项1' }, { id: 2, title: '测试项2' }, ] })); describe('列表性能测试', () => { it('仅被操作子项触发重渲染', () => { const childRenderSpy = jest.spyOn(Child.type, 'render'); const wrapper = mount(<Parent />); expect(childRenderSpy).toHaveBeenCalledTimes(2); childRenderSpy.mockClear(); // 触发第一个子项的点击 wrapper.find('button').at(0).simulate('click'); wrapper.update(); expect(childRenderSpy).toHaveBeenCalledTimes(1); }); });
注意事项
- 测试时不要包裹
React.StrictMode,严格模式的双渲染机制会导致计数统计错误 - 如果状态更新是异步逻辑,必须加
waitFor等待更新完成后再校验渲染次数 - 当测试失败时,说明子组件收到了引用变化的props,刚好可以命中你要拦截的场景:比如传了匿名函数、未用useMemo包裹的对象等
- 如果你自定义了memo的第二个参数比较函数,可以单独写单元测试验证比较逻辑的正确性
内容的提问来源于stack exchange,提问作者jmancherje
相关产品推荐
相关产品推荐

