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 的局限性 #

已知限制 #

  1. 无法测试内部状态:有时确实需要验证内部状态
  2. 无法测试私有方法:无法直接测试组件内部方法
  3. 学习曲线:需要理解无障碍性概念

解决方案 #

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