You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 21:06:04