Skip to content

用 Jest 测试 React 组件:从单组件到多组件联动

更新: 6/14/2026 字数: 0 字 时长: 0 分钟

一、先搞清楚:测 React 组件,到底测什么

测一个普通函数很简单——给输入、看输出。但 React 组件不一样,它会渲染界面、响应点击、管理状态、和别的组件互动。所以测组件,本质上是回答三个问题:

  1. 渲染对不对:组件有没有把该显示的内容显示出来?
  2. 交互对不对:用户点击、输入后,界面有没有正确响应?
  3. 协作对不对:多个组件凑在一起,数据传递和联动有没有出错?

光靠 Jest 本身做不到这些,因为它跑在 Node 环境里,没有浏览器的 DOM。所以要给它配两个搭档。

用 Jest 测 React 组件

二、准备工作:三件套搭档

测 React 组件的标准组合是:

  • Jest:测试运行器 + 断言 + Mock,负责"跑测试、下判断"。
  • jsdom:在内存里模拟一套浏览器 DOM,让组件能"假装"渲染出来。新版 Jest 里通过配置 testEnvironment: 'jsdom' 开启。
  • React Testing Library(RTL):专门用来渲染组件、查找元素、模拟用户操作的库。

安装:

bash
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom

在 Jest 配置里指定环境(jest.config.js):

javascript
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['@testing-library/jest-dom'],
};

@testing-library/jest-dom 提供了 toBeInTheDocument()toHaveTextContent() 这类专门针对 DOM 的好用断言,强烈建议配上。

三、核心理念:像用户一样思考

测行为,不测实现细节

这是 React Testing Library 最重要的设计哲学,也是新手最容易走偏的地方:

测组件"对用户表现出的行为",而不是"内部怎么实现的"。

什么意思?用户不关心你组件内部有个叫 count 的 state、用了 useReducer 还是 useState。用户只关心:屏幕上显示了什么、点了按钮后变成了什么。

所以好的测试应该:

  • ✅ 查找"用户能看到的东西":按钮文字、标题、输入框
  • ✅ 模拟"用户真实的操作":点击、输入、勾选
  • ❌ 不要去断言组件内部的 state 值、私有方法

这样做的最大好处是:重构时测试不会动不动就崩。 你把 useState 换成 useReducer,只要界面行为没变,测试照样全绿——因为它测的是行为,不是实现。

四、实战:测一个单组件

假设有个计数器组件 Counter.jsx:

jsx
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>当前:{count}</p>
      <button onClick={() => setCount(count + 1)}>加一</button>
    </div>
  );
}

测试文件 Counter.test.jsx:

jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

test('初始显示为 0', () => {
  render(<Counter />);
  expect(screen.getByText('当前:0')).toBeInTheDocument();
});

test('点击加一按钮后计数增加', async () => {
  const user = userEvent.setup();
  render(<Counter />);

  await user.click(screen.getByText('加一'));

  expect(screen.getByText('当前:1')).toBeInTheDocument();
});

拆解几个关键 API:

  • render():把组件渲染到 jsdom 里。
  • screen:从渲染结果中查找元素的入口。
  • userEvent:模拟真实用户操作,比老的 fireEvent 更接近真实(会模拟完整的交互过程),推荐优先用它。

查找元素的几种方式(按推荐优先级)

查询方法适用场景推荐度
getByRole按无障碍角色找(button、heading 等),最贴近用户⭐⭐⭐ 首选
getByLabelText找表单输入框(配合 label)⭐⭐⭐ 表单首选
getByText按显示文字找⭐⭐ 常用
getByTestId实在没法找时,加 data-testid 兜底⭐ 最后手段

小贴士:优先用 getByRole,因为它逼着你写出对无障碍友好的组件,一举两得。getByTestId 是"逃生通道",别滥用。

getBy / queryBy / findBy 别搞混

  • getByXxx:找不到就报错——用于"确信它存在"。
  • queryByXxx:找不到返回 null,不报错——用于"断言某元素不存在"。
  • findByXxx:返回 Promise,会等待元素出现——用于异步场景。
javascript
// 断言某元素不存在,必须用 queryBy
expect(screen.queryByText('错误提示')).not.toBeInTheDocument();

五、多组件联动测试

多组件怎么联动测

实际项目里,组件很少单独存在。常见的联动场景:

  • 父组件通过 props 把数据/回调传给子组件
  • 子组件 A 操作后,子组件 B 跟着变化(通过父组件的状态)
  • 多个组件共享 Context 或全局状态(Redux / Zustand)

联动测试的核心思路:不要把组件拆开单独测,而是把它们"一起渲染",然后模拟用户操作,验证整体的协作结果。 这其实就跨入了"集成测试"的范畴。

举个例子:一个搜索框 SearchBox + 结果列表 ResultList,都被父组件 SearchPage 管理。用户在搜索框输入后,列表应该过滤。

jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SearchPage from './SearchPage';

test('输入关键词后,列表只显示匹配项', async () => {
  const user = userEvent.setup();
  // 关键:渲染父组件,让子组件们一起工作
  render(<SearchPage />);

  // 在搜索框(子组件A)输入
  await user.type(screen.getByLabelText('搜索'), '苹果');

  // 验证结果列表(子组件B)的联动变化
  expect(screen.getByText('苹果')).toBeInTheDocument();
  expect(screen.queryByText('香蕉')).not.toBeInTheDocument();
});

注意:这里我们渲染的是父组件 SearchPage,而不是单独渲染 SearchBoxResultList。这样才能测出它们之间真实的数据流动。

六、多组件联动测试的五个注意点

联动测试要避开的坑

1. 整体渲染,别孤立测

联动测试要从"承载状态的那一层"开始渲染(通常是父组件或页面组件),让子组件在真实的上下文里协作。如果你把每个子组件抽出来单独测,就永远测不到它们之间的联动。

2. 异步更新一定要 await

组件联动常伴随异步(接口请求、状态更新后的重渲染)。直接断言会"扑空",因为界面还没更新完。要用 findBywaitFor 等待:

javascript
// 等待异步出现的元素
expect(await screen.findByText('加载完成')).toBeInTheDocument();

// 或用 waitFor 等待某个条件成立
import { waitFor } from '@testing-library/react';
await waitFor(() => {
  expect(screen.getByText('订单已提交')).toBeInTheDocument();
});

另外,凡是会触发 state 更新的操作,用 userEvent 时记得 await,否则会报 act(...) 警告——这个警告几乎都是"没等异步更新完"导致的。

3. 外部依赖要 Mock 掉

联动组件常会调接口、用浏览器 API。单元/集成测试里要把这些换成"替身",既快又稳定,还能模拟各种返回:

javascript
// Mock 掉网络请求模块
jest.mock('./api');
import { fetchList } from './api';

beforeEach(() => {
  fetchList.mockResolvedValue([{ id: 1, name: '苹果' }]);
});

这样测试不依赖真实网络,还能轻松构造"接口报错""返回空"等边界场景。

4. 共享状态要包裹 Provider

如果组件依赖 Context、Redux、路由,单独渲染会报错(找不到 Provider)。要在 render 时套上所需的 Provider:

jsx
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';

render(
  <Provider store={testStore}>
    <BrowserRouter>
      <SearchPage />
    </BrowserRouter>
  </Provider>
);

实战建议:封装一个 renderWithProviders 工具函数,把这些 Provider 统一包好,避免每个测试重复写一大坨。

5. 测试间状态要隔离

多个测试共用一套环境,上一个测试改了全局状态/Mock,会污染下一个。务必清理:

javascript
afterEach(() => {
  jest.clearAllMocks();  // 清空 Mock 的调用记录
  // 如果用了全局 store,这里重置它
});

RTL 会在每个测试后自动卸载组件(cleanup),但 Mock 和全局状态需要你自己重置。测试之间互不影响,是可靠测试的底线。

七、单组件 vs 多组件联动:一张表对比

维度单组件测试多组件联动测试
渲染对象单个组件父组件 / 页面组件(连带子组件)
关注点单个组件的渲染和交互组件间的数据传递与协作
依赖处理Mock 自身依赖即可需 Mock 接口 + 包裹 Provider
异步处理较少频繁,必须 await / waitFor
本质归类单元测试偏集成测试

八、几条实战建议收尾

  • 从用户视角写:断言"看得见的内容",而非内部 state,测试才稳。
  • 优先 getByRole + userEvent:既贴近真实,又顺带提升无障碍质量。
  • 异步必等待:findBy / waitFor 是联动测试的命根子,漏了就会偶发性失败(flaky)。
  • 封装 render 工具:把 Provider 和常用配置抽成 renderWithProviders,省心又统一。
  • 保持测试独立:每个测试自带准备和清理,不依赖运行顺序。
  • 别越界:Jest + RTL 能测到"模拟环境里的组件联动",但跨页面跳转、真实后端的完整流程,该交给 Cypress / Playwright 做端到端测试。

九、一句话总结

用 Jest 测 React 组件,记住一条主线:像真实用户那样去操作,只断言用户看得见的结果。 单组件测的是"它自己表现对不对",多组件联动测的是"它们凑在一起协作得好不好"——后者的关键在于整体渲染、耐心等异步、Mock 掉外部、包好 Provider、测完即清理。把这五点做扎实,你的组件测试就能既贴近真实,又稳定不抽风。