如何在 Jest 中测试 React 组件?常用的测试工具和查询方法有哪些?
在 Jest 中测试 React 组件,核心思路是:渲染组件 → 查询元素 → 断言行为。React 官方推荐的测试方案是 Jest + React Testing Library(RTL),本文聚焦面试中高频考察的知识点。
React 组件测试的基本流程是什么?
测试 React 组件通常分三步:
- 渲染:使用 RTL 的
render方法将组件挂载到虚拟 DOM - 查询:通过
screen对象提供的方法定位页面元素 - 断言:使用 Jest 的
expect验证元素状态或行为
javascriptimport { 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* | 返回 Promise | Promise reject | 异步元素出现 |
具体查询方法的推荐优先级:
getByRole— 最优先,基于 ARIA 角色,如button、textbox、headinggetByLabelText— 表单元素优先用,关联 label 文本getByPlaceholderText— 没有 label 时使用getByText— 非表单元素(按钮、链接、段落)常用getByTestId— 最后手段,需要手动添加data-testid属性
javascript// 推荐:通过角色查询 screen.getByRole('button', { name: /submit/i }); // 不推荐但有时必要:通过 testId 查询 screen.getByTestId('submit-btn');
面试关键点:优先使用 getByRole 是因为它验证了组件的可访问性,这与 RTL "测试用户视角" 的核心理念一致。
如何测试用户交互?
使用 fireEvent 或 userEvent 模拟用户操作。userEvent 更接近真实用户行为,推荐优先使用。
javascriptimport { 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(); });
fireEvent 与 userEvent 的区别:
fireEvent.click()只触发 click 事件userEvent.click()会依次触发 mousedown → mouseup → focus → click,更贴近真实操作userEvent.type()会逐字符触发键盘事件,而fireEvent.change()直接修改值
异步组件怎么测试?
异步场景(接口请求、定时器、状态延迟更新)使用 waitFor 或 findBy* 处理。
javascriptimport { 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 函数
javascripttest('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 进行测试:
javascriptimport { 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 包。
快照测试怎么用?什么场景下用?
javascriptimport renderer from 'react-test-renderer'; test('Button matches snapshot', () => { const tree = renderer.create(<Button>Click</Button>).toJSON(); expect(tree).toMatchSnapshot(); });
快照测试的适用与不适用:
- 适合:配置型组件(Theme、Layout),结构稳定的纯展示组件
- 不适合:频繁变动的业务组件,否则每次改动都要更新快照,失去测试价值
面试加分点:快照测试只是确认结构没变,并不验证行为是否正确,所以不能替代行为测试。
测试 React 组件有哪些最佳实践?
- 测试行为,不测实现 — 不测内部 state 的值,测用户看到的结果
- 避免过度 Mock — Mock 越多,测试离真实场景越远
- 查询方法按优先级选 —
getByRole>getByLabelText>getByText>getByTestId - 异步用
findBy优于waitFor+getBy— 更简洁,语义更清晰 - 使用
screen而非render返回值 — 避免反复解构,代码更干净 - 一个测试只验证一个行为 — 方便定位失败原因
面试追问方向:如何测试 Context Provider 包裹的组件?如何处理第三方库的渲染行为?如何在 CI 中提升测试执行速度?这些是区分中级与高级的关键问题。