在 React 测试体系中,这三种测试覆盖了组件的核心验证维度:是否按预期渲染、响应用户操作后行为是否正确、以及 UI 结构是否意外变更。以下结合 Jest 和 React Testing Library 给出实用示例与原则。
组件渲染测试
渲染测试关注组件的“静态输出”:给定某些 Props 和状态,组件渲染的 DOM 是否包含预期的元素、文本或属性。
核心 API:render、screen、getByText、getByRole 等查询方法。
示例:测试一个 UserCard 组件
// UserCard.jsx
function UserCard({ name, email, isActive }) {
return (
<div className="user-card">
<h2>{name}</h2>
{email && <p>{email}</p>}
<span>{isActive ? '激活' : '未激活'}</span>
</div>
);
}
测试代码:
import { render, screen } from '@testing-library/react';
import UserCard from './UserCard';
test('渲染用户名、邮箱和激活状态', () => {
render(<UserCard name="张三" email="zhangsan@example.com" isActive={true} />);
// 查询元素
expect(screen.getByText('张三')).toBeInTheDocument();
expect(screen.getByText('zhangsan@example.com')).toBeInTheDocument();
expect(screen.getByText('激活')).toBeInTheDocument();
});
test('未传 email 时不渲染邮箱段落', () => {
render(<UserCard name="李四" isActive={false} />);
expect(screen.queryByText('zhangsan@example.com')).toBeNull(); // 或 .not.toBeInTheDocument()
expect(screen.getByText('未激活')).toBeInTheDocument();
});
原则:
- 优先使用可访问性查询(
getByRole、getByLabelText),模拟用户是如何找到元素的。 - 使用
queryBy...而非getBy...来验证元素不存在(getBy...找不到会直接抛出错误)。 - 测试与业务需求相关的内容,而非实现细节(如 CSS 类名),除非类名本身具有语义(如
.error)。
交互测试
交互测试模拟用户操作(点击、输入、选择等),验证组件的行为是否正确触发:回调是否调用、状态是否改变、UI 是否按预期更新。
工具:fireEvent(简单事件)或 @testing-library/user-event(推荐,更接近真实用户行为)。
示例:测试一个计数器组件
// Counter.jsx
function Counter({ initial = 0, onChange }) {
const [count, setCount] = useState(initial);
const increment = () => {
const newCount = count + 1;
setCount(newCount);
onChange?.(newCount);
};
return (
<div>
<p>计数:{count}</p>
<button onClick={increment}>增加</button>
</div>
);
}
测试代码:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
test('点击增加按钮后计数加1并触发回调', async () => {
const handleChange = jest.fn();
render(<Counter initial={5} onChange={handleChange} />);
const button = screen.getByRole('button', { name: '增加' });
await userEvent.click(button);
expect(screen.getByText('计数:6')).toBeInTheDocument();
expect(handleChange).toHaveBeenCalledWith(6);
});
操作类型覆盖:
userEvent.type(input, 'text'):模拟输入userEvent.click()userEvent.selectOptions()userEvent.tab()、keyboard('{Enter}')等
注意点:
- userEvent 的每个操作是异步的,记得使用
await。 - 验证状态变更后的 UI 可配合
waitFor或findBy*查询,处理异步渲染。
原则:
- 以用户视角测试,避免直接调用组件内部方法或状态修改。
- 对于表单提交等复杂流程,可测试完整交互链。
- 测试孤独:每个测试用例不依赖其他用例的执行顺序。
快照测试
快照测试用于捕获组件渲染结构的“快照”,并在后续测试中对比是否有意外变化。主要用于防止 UI 结构的非预期退化。
实现:Jest 提供 toMatchSnapshot()。
示例:测试列表组件结构
// ItemList.jsx
function ItemList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
测试代码:
import { render } from '@testing-library/react';
import ItemList from './ItemList';
test('渲染静态列表快照', () => {
const items = [
{ id: 1, text: '任务A' },
{ id: 2, text: '任务B' }
];
const { container } = render(<ItemList items={items} />);
expect(container.firstChild).toMatchSnapshot();
});
首次运行会在 snapshots 目录生成快照文件(.snap)。后续测试会对比渲染输出是否完全一致,不一致则提示更新或检查。
何时使用快照测试:
- 组件较小且结构稳定,一次性验证整体结构。
- 纯展示组件(如 Footer、Empty 状态)。
- 需要快速回归测试 UI 的时候。
何时慎用或避免:
- 大型组件或结构频繁变动的组件(更新快照太频繁)。
- 动态内容(如时间戳、随机数)会导致每次快照不同,需用
toMatchSnapshot({...})忽略特定属性。 - 快照不能替代行为测试;它只验证结构不变,不验证逻辑正确。
最佳实践:
- 将快照文件纳入版本控制(Git),在代码评审中确认结构变更。
- 结合
toMatchInlineSnapshot()可将快照内联在测试代码中,更易读。 - 不建议将整个页面的快照作为唯一测试手段,容易形成“快照垃圾”。
小结合与
一个完善的测试套件通常组合使用这三种测试:
- 渲染测试:验证所有关键元素出现/不出现。
- 交互测试:验证用户操作驱动状态变化后的 UI 更新及回调。
- 快照测试:守护 UI 结构不意外退化,尤其适用于常量组件。
React Testing Library 的核心哲学是“像用户一样测试组件”,这贯穿于所有测试类型。牢记这一点,能让你避免测试实现细节,写出健壮且有意义的测试用例。