Testing Library 简介 #
什么是 Testing Library? #
Testing Library 是一组用于测试 Web 组件的实用工具库,由 Kent C. Dodds 创建。它的核心理念是"测试越接近用户使用软件的方式,测试就越可靠"。Testing Library 不关注组件的实现细节,而是关注用户如何与组件交互。
核心定位 #
text
┌─────────────────────────────────────────────────────────────┐
│ Testing Library │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ DOM 查询 │ │ 用户交互 │ │ 异步处理 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 无障碍查询 │ │ 事件触发 │ │ 等待工具 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
Testing Library 的历史 #
发展历程 #
text
2018年 ─── Testing Library 项目启动
│
│ Kent C. Dodds 创建
│ 专注于 React 组件测试
│
2019年 ─── 多框架支持
│
│ @testing-library/dom
│ @testing-library/vue
│ @testing-library/angular
│
2020年 ─── React Testing Library 成为主流
│
│ Create React App 默认集成
│ React 官方推荐
│
2021年 ─── 生态系统成熟
│
│ Jest DOM 扩展
│ User Event 增强
│ Testing Library Jest 集成
│
2023年 ─── 持续演进
│
│ React 18 支持
│ 更好的异步工具
│ 性能优化
│
至今 ─── 行业标准
│
│ 超过 2000 万周下载量
│ React 测试首选方案
里程碑版本 #
| 版本 | 时间 | 重要特性 |
|---|---|---|
| 1.0 | 2018 | 基础 DOM 查询功能 |
| 2.0 | 2019 | 多框架支持 |
| 3.0 | 2019 | 异步工具增强 |
| 5.0 | 2020 | within 和 screen API |
| 8.0 | 2021 | React 18 支持 |
| 12.0 | 2022 | 改进的类型支持 |
| 14.0 | 2023 | 性能优化 |
为什么选择 Testing Library? #
传统测试的痛点 #
在 Testing Library 出现之前,React 组件测试面临以下问题:
javascript
// Enzyme 风格 - 关注实现细节
import { shallow } from 'enzyme';
test('Counter increments', () => {
const wrapper = shallow(<Counter />);
expect(wrapper.state('count')).toBe(0);
wrapper.find('button').simulate('click');
expect(wrapper.state('count')).toBe(1);
});
// 问题:
// 1. 测试组件内部状态
// 2. 测试组件结构
// 3. 重构时测试容易失败
// 4. 不关注用户体验
Testing Library 的解决方案 #
javascript
// Testing Library 风格 - 关注用户行为
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('Counter increments', async () => {
const user = userEvent.setup();
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
// 优势:
// 1. 测试用户看到的内容
// 2. 测试用户交互行为
// 3. 重构不影响测试
// 4. 更接近真实使用场景
Testing Library 的核心理念 #
1. 以用户为中心 #
测试应该模拟用户的行为,而不是测试实现细节:
javascript
// ❌ 不好的做法 - 测试实现细节
test('sets state correctly', () => {
const wrapper = shallow(<LoginForm />);
wrapper.instance().setState({ email: 'test@test.com' });
expect(wrapper.state('email')).toBe('test@test.com');
});
// ✅ 好的做法 - 测试用户行为
test('submits form with user input', async () => {
const user = userEvent.setup();
render(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'test@test.com');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.getByText('Welcome!')).toBeInTheDocument();
});
2. 查询优先级 #
按照用户感知的方式查询元素:
text
┌─────────────────────────────────────────────────────────────┐
│ 查询优先级 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. getByRole ← 最推荐(无障碍) │
│ getByLabelText │
│ getByPlaceholderText │
│ getByText │
│ getByDisplayValue │
│ │
│ 2. getByAltText ← 语义化 │
│ getByTitle │
│ │
│ 3. getByTestId ← 最后手段 │
│ │
└─────────────────────────────────────────────────────────────┘
3. 避免实现细节 #
不测试以下内容:
javascript
// ❌ 避免测试的内容
test('component state', () => {
// 组件内部状态
expect(wrapper.state('isOpen')).toBe(true);
});
test('component methods', () => {
// 组件方法
expect(wrapper.instance().handleClick).toHaveBeenCalled();
});
test('component structure', () => {
// 组件 DOM 结构
expect(wrapper.find('div.button-container').length).toBe(1);
});
// ✅ 应该测试的内容
test('user can toggle menu', async () => {
const user = userEvent.setup();
render(<Menu />);
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /open menu/i }));
expect(screen.getByRole('menu')).toBeVisible();
});
Testing Library 家族 #
核心库 #
text
@testing-library/dom
├── @testing-library/react # React 支持
├── @testing-library/vue # Vue 支持
├── @testing-library/angular # Angular 支持
├── @testing-library/svelte # Svelte 支持
└── @testing-library/preact # Preact 支持
配套工具 #
| 包名 | 用途 |
|---|---|
| @testing-library/jest-dom | Jest 自定义匹配器 |
| @testing-library/user-event | 用户交互模拟 |
| @testing-library/react-hooks | Hooks 测试(已弃用) |
Testing Library 与 Enzyme 对比 #
设计理念对比 #
| 方面 | Testing Library | Enzyme |
|---|---|---|
| 关注点 | 用户行为 | 实现细节 |
| 查询方式 | 无障碍查询 | DOM 结构 |
| 重构友好 | ✅ 是 | ❌ 否 |
| 学习曲线 | 低 | 中 |
| 维护状态 | 活跃 | 已停止 |
代码对比 #
javascript
// Enzyme 方式
import { mount } from 'enzyme';
test('TodoList adds item', () => {
const wrapper = mount(<TodoList />);
const input = wrapper.find('input');
input.simulate('change', { target: { value: 'New item' } });
wrapper.find('form').simulate('submit');
expect(wrapper.find('li').length).toBe(1);
});
// Testing Library 方式
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('TodoList adds item', async () => {
const user = userEvent.setup();
render(<TodoList />);
await user.type(screen.getByRole('textbox'), 'New item');
await user.click(screen.getByRole('button', { name: /add/i }));
expect(screen.getByText('New item')).toBeInTheDocument();
});
Testing Library 的应用场景 #
1. 组件渲染测试 #
测试组件是否正确渲染:
javascript
import { render, screen } from '@testing-library/react';
import Button from './Button';
test('renders button with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
2. 用户交互测试 #
测试用户交互行为:
javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
test('increments counter on click', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
await user.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
3. 异步操作测试 #
测试异步行为:
javascript
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
test('displays users after loading', async () => {
render(<UserList />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
4. 表单测试 #
测试表单输入和验证:
javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
test('validates required fields', async () => {
const user = userEvent.setup();
render(<LoginForm />);
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});
Testing Library 的核心概念 #
render 函数 #
将组件渲染到测试容器中:
javascript
import { render } from '@testing-library/react';
const { container, unmount, rerender } = render(<App />);
screen 对象 #
提供所有查询方法的便捷访问:
javascript
import { screen } from '@testing-library/react';
screen.getByText('Hello');
screen.getByRole('button');
screen.getByLabelText('Email');
查询方法 #
三种类型的查询:
javascript
// getBy* - 获取一个元素,找不到则报错
screen.getByRole('button');
// queryBy* - 获取一个元素,找不到返回 null
screen.queryByRole('dialog');
// findBy* - 异步获取元素,返回 Promise
await screen.findByText('Loaded');
用户事件 #
模拟用户交互:
javascript
import userEvent from '@testing-library/user-event';
const user = userEvent.setup();
await user.click(button);
await user.type(input, 'hello');
await user.selectOptions(select, 'option1');
Testing Library 的设计哲学 #
1. 测试应该是用户视角 #
javascript
// 用户看到的是文本,不是 DOM 结构
expect(screen.getByText('Welcome')).toBeInTheDocument();
// 用户点击的是按钮,不是类名
await user.click(screen.getByRole('button'));
// 用户阅读的是标签,不是 input ID
await user.type(screen.getByLabelText('Email'), 'test@test.com');
2. 测试应该是可维护的 #
javascript
// ❌ 脆弱的测试 - 依赖实现细节
test('shows error message', () => {
const wrapper = mount(<Form />);
expect(wrapper.find('.error-message').text()).toBe('Invalid input');
});
// ✅ 可维护的测试 - 不依赖实现
test('shows error message', () => {
render(<Form />);
expect(screen.getByRole('alert')).toHaveTextContent('Invalid input');
});
3. 测试应该是有意义的 #
javascript
// ❌ 无意义的测试
test('renders div', () => {
const { container } = render(<App />);
expect(container.querySelector('div')).toBeInTheDocument();
});
// ✅ 有意义的测试
test('renders welcome message', () => {
render(<App />);
expect(screen.getByRole('heading', { name: /welcome/i })).toBeInTheDocument();
});
Testing Library 的优势 #
1. 更可靠的测试 #
text
实现细节测试:
修改 CSS 类名 → 测试失败 ❌
重构组件结构 → 测试失败 ❌
更新状态管理 → 测试失败 ❌
用户行为测试:
修改 CSS 类名 → 测试通过 ✅
重构组件结构 → 测试通过 ✅
更新状态管理 → 测试通过 ✅
2. 更好的无障碍性 #
javascript
// 使用 role 查询会提醒你添加无障碍属性
screen.getByRole('button'); // 提示添加 role="button"
screen.getByLabelText('Email'); // 提示关联 label
3. 更简单的 API #
javascript
// Enzyme 需要学习多个 API
wrapper.find()
wrapper.state()
wrapper.props()
wrapper.instance()
wrapper.simulate()
// Testing Library 只需要几个核心概念
screen.getBy*()
user.click()
user.type()
waitFor()
Testing Library 的局限性 #
已知限制 #
- 无法测试内部状态:有时确实需要验证内部状态
- 无法测试私有方法:无法直接测试组件内部方法
- 学习曲线:需要理解无障碍性概念
解决方案 #
javascript
// 1. 通过 UI 验证状态
// 状态变化应该反映在 UI 上
expect(screen.getByText('Count: 5')).toBeInTheDocument();
// 2. 通过回调验证行为
const onChange = jest.fn();
render(<Input onChange={onChange} />);
await user.type(screen.getByRole('textbox'), 'test');
expect(onChange).toHaveBeenCalled();
// 3. 学习无障碍性
// 了解 ARIA roles 和属性
学习路径 #
text
入门阶段
├── 安装与配置
├── 编写第一个测试
├── 使用查询方法
└── 理解查询优先级
进阶阶段
├── 用户交互测试
├── 异步操作测试
├── 表单测试
└── 事件处理
高级阶段
├── Context 测试
├── Hooks 测试
├── 自定义渲染器
└── 测试工具函数
实战阶段
├── 组件库测试
├── 应用测试
├── 集成测试
└── 最佳实践
下一步 #
现在你已经了解了 Testing Library 的基本概念,接下来学习 安装与配置 开始实际使用 Testing Library!
最后更新:2026-03-28