5月28日 07:00

如何在 Jest 中测试 React 组件?常用的测试工具和查询方法有哪些?

在 Jest 中测试 React 组件,核心思路是:渲染组件 → 查询元素 → 断言行为。React 官方推荐的测试方案是 Jest + React Testing Library(RTL),本文聚焦面试中高频考察的知识点。

React 组件测试的基本流程是什么?

测试 React 组件通常分三步:

  1. 渲染:使用 RTL 的 render 方法将组件挂载到虚拟 DOM
  2. 查询:通过 screen 对象提供的方法定位页面元素
  3. 断言:使用 Jest 的 expect 验证元素状态或行为
javascript
import { render, screen } from '@testing-library/react'; import Counter from './Counter'; test('counter displays initial value', () => { render(<Counter initialCount={0} />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); });

查询方法的优先级怎么选?

RTL 的查询方法有三个前缀,区别在于元素不存在时的行为:

前缀元素存在元素不存在适用场景
getBy*返回元素抛出错误断言元素一定存在
queryBy*返回元素返回 null断言元素不存在
findBy*返回 PromisePromise reject异步元素出现

具体查询方法的推荐优先级:

  1. getByRole — 最优先,基于 ARIA 角色,如 buttontextboxheading
  2. getByLabelText — 表单元素优先用,关联 label 文本
  3. getByPlaceholderText — 没有 label 时使用
  4. getByText — 非表单元素(按钮、链接、段落)常用
  5. getByTestId — 最后手段,需要手动添加 data-testid 属性
javascript
// 推荐:通过角色查询 screen.getByRole('button', { name: /submit/i }); // 不推荐但有时必要:通过 testId 查询 screen.getByTestId('submit-btn');

面试关键点:优先使用 getByRole 是因为它验证了组件的可访问性,这与 RTL "测试用户视角" 的核心理念一致。

如何测试用户交互?

使用 fireEventuserEvent 模拟用户操作。userEvent 更接近真实用户行为,推荐优先使用。

javascript
import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; test('clicking button increments counter', async () => { const user = userEvent.setup(); render(<Counter initialCount={0} />); await user.click(screen.getByRole('button', { name: /increment/i })); expect(screen.getByText('Count: 1')).toBeInTheDocument(); });

fireEventuserEvent 的区别:

  • fireEvent.click() 只触发 click 事件
  • userEvent.click() 会依次触发 mousedown → mouseup → focus → click,更贴近真实操作
  • userEvent.type() 会逐字符触发键盘事件,而 fireEvent.change() 直接修改值

异步组件怎么测试?

异步场景(接口请求、定时器、状态延迟更新)使用 waitForfindBy* 处理。

javascript
import { render, screen, waitFor } from '@testing-library/react'; test('displays user data after loading', async () => { render(<UserProfile userId={1} />); // 方式一:findBy(推荐,更简洁) expect(await screen.findByText('John')).toBeInTheDocument(); // 方式二:waitFor(更灵活,可组合多个断言) await waitFor(() => { expect(screen.getByText('John')).toBeInTheDocument(); expect(screen.getByText('john@example.com')).toBeInTheDocument(); }); });

常见坑waitFor 中不要用 queryBy*,因为它不抛错,断言不会失败,导致测试误通过。应使用 getBy*

如何 Mock 模块和 API 请求?

面试中常考的 Mock 手段分两种:

Jest.fn() — Mock 函数

javascript
test('calls onSubmit with form data', async () => { const onSubmit = jest.fn(); const user = userEvent.setup(); render(<LoginForm onSubmit={onSubmit} />); await user.type(screen.getByLabelText(/email/i), 'test@example.com'); await user.click(screen.getByRole('button', { name: /login/i })); expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com' }); });

jest.mock — Mock 模块

javascript
// Mock API 请求模块 jest.mock('../api', () => ({ fetchUser: jest.fn().mockResolvedValue({ name: 'John' }) })); test('renders fetched user name', async () => { render(<UserProfile />); expect(await screen.findByText('John')).toBeInTheDocument(); });

对于更复杂的 API Mock 场景,可以使用 Mock Service Worker(MSW),它在 Service Worker 层拦截请求,不需要修改业务代码。

React Hooks 怎么测试?

自定义 Hook 使用 renderHook 进行测试:

javascript
import { renderHook, act } from '@testing-library/react'; import { useCounter } from './useCounter'; test('useCounter increments and decrements', () => { const { result } = renderHook(() => useCounter(0)); expect(result.current.count).toBe(0); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); act(() => { result.current.decrement(); }); expect(result.current.count).toBe(0); });

注意:状态更新必须包裹在 act() 中,否则 Jest 会报警告。renderHook 已从 RTL v13 起内置,不再需要 @testing-library/react-hooks 包。

快照测试怎么用?什么场景下用?

javascript
import renderer from 'react-test-renderer'; test('Button matches snapshot', () => { const tree = renderer.create(<Button>Click</Button>).toJSON(); expect(tree).toMatchSnapshot(); });

快照测试的适用与不适用:

  • 适合:配置型组件(Theme、Layout),结构稳定的纯展示组件
  • 不适合:频繁变动的业务组件,否则每次改动都要更新快照,失去测试价值

面试加分点:快照测试只是确认结构没变,并不验证行为是否正确,所以不能替代行为测试。

测试 React 组件有哪些最佳实践?

  1. 测试行为,不测实现 — 不测内部 state 的值,测用户看到的结果
  2. 避免过度 Mock — Mock 越多,测试离真实场景越远
  3. 查询方法按优先级选getByRole > getByLabelText > getByText > getByTestId
  4. 异步用 findBy 优于 waitFor + getBy — 更简洁,语义更清晰
  5. 使用 screen 而非 render 返回值 — 避免反复解构,代码更干净
  6. 一个测试只验证一个行为 — 方便定位失败原因

面试追问方向:如何测试 Context Provider 包裹的组件?如何处理第三方库的渲染行为?如何在 CI 中提升测试执行速度?这些是区分中级与高级的关键问题。

标签:Jest