Jest DOM 测试 #
DOM 测试概述 #
DOM 测试是前端测试的重要组成部分,用于验证 UI 组件的渲染和交互行为。
text
┌─────────────────────────────────────────────────────────────┐
│ DOM 测试内容 │
├─────────────────────────────────────────────────────────────┤
│ 1. 元素渲染 - 检查元素是否存在 │
│ 2. 内容验证 - 检查文本和属性 │
│ 3. 样式检查 - 验证 CSS 类和样式 │
│ 4. 用户交互 - 模拟点击、输入等 │
│ 5. 事件处理 - 验证事件回调 │
└─────────────────────────────────────────────────────────────┘
环境配置 #
安装依赖 #
bash
# 安装 Jest 和 Testing Library
npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
# Vue 项目
npm install --save-dev @vue/test-utils @testing-library/vue
配置 jsdom #
javascript
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
javascript
// jest.setup.js
import '@testing-library/jest-dom';
基本查询 #
查询方法 #
javascript
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
// getBy - 获取单个元素,找不到会报错
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
// queryBy - 获取单个元素,找不到返回 null
const missingElement = screen.queryByText(/missing/i);
expect(missingElement).not.toBeInTheDocument();
// findBy - 异步获取元素
const asyncElement = await screen.findByText(/loaded/i);
expect(asyncElement).toBeInTheDocument();
});
查询类型 #
javascript
test('query types', () => {
render(<UserCard user={{ name: 'John', email: 'john@example.com' }} />);
// 按文本查询
screen.getByText('John');
// 按角色查询
screen.getByRole('button');
screen.getByRole('heading', { name: /profile/i });
// 按标签查询
screen.getByLabelText('Email');
// 按占位符查询
screen.getByPlaceholderText('Enter name');
// 按测试 ID 查询
screen.getByTestId('user-card');
// 按显示值查询
screen.getByDisplayValue('John');
});
断言方法 #
jest-dom 扩展匹配器 #
javascript
import { render, screen } from '@testing-library/react';
import Button from './Button';
test('button assertions', () => {
render(<Button disabled>Click me</Button>);
const button = screen.getByRole('button');
// 存在性
expect(button).toBeInTheDocument();
// 可见性
expect(button).toBeVisible();
// 禁用状态
expect(button).toBeDisabled();
// 启用状态
expect(button).not.toBeEnabled();
// 文本内容
expect(button).toHaveTextContent('Click me');
// 类名
expect(button).toHaveClass('btn-primary');
// 属性
expect(button).toHaveAttribute('type', 'button');
});
更多断言 #
javascript
test('more assertions', () => {
render(
<div>
<input type="text" value="test" readOnly />
<a href="https://example.com">Link</a>
<button style={{ color: 'red' }}>Red Button</button>
<span className="error">Error message</span>
</div>
);
// 输入值
const input = screen.getByRole('textbox');
expect(input).toHaveValue('test');
// 链接
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', 'https://example.com');
// 样式
const button = screen.getByText('Red Button');
expect(button).toHaveStyle({ color: 'red' });
// 焦点
input.focus();
expect(input).toHaveFocus();
// 错误消息
const error = screen.getByText('Error message');
expect(error).toHaveClass('error');
});
用户交互 #
点击事件 #
javascript
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('counter increments on click', () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
const count = screen.getByText('0');
fireEvent.click(button);
expect(count).toHaveTextContent('1');
fireEvent.click(button);
expect(count).toHaveTextContent('2');
});
输入事件 #
javascript
import { render, screen, fireEvent } from '@testing-library/react';
import SearchForm from './SearchForm';
test('search form input', () => {
const onSearch = jest.fn();
render(<SearchForm onSearch={onSearch} />);
const input = screen.getByPlaceholderText('Search...');
fireEvent.change(input, { target: { value: 'react' } });
expect(input).toHaveValue('react');
const button = screen.getByRole('button', { name: /search/i });
fireEvent.click(button);
expect(onSearch).toHaveBeenCalledWith('react');
});
表单提交 #
javascript
import { render, screen, fireEvent } from '@testing-library/react';
import LoginForm from './LoginForm';
test('form submission', () => {
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByLabelText('Email'), {
target: { value: 'test@example.com' },
});
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'password123' },
});
fireEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
使用 userEvent #
bash
npm install --save-dev @testing-library/user-event
javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SearchForm from './SearchForm';
test('search with userEvent', async () => {
const user = userEvent.setup();
const onSearch = jest.fn();
render(<SearchForm onSearch={onSearch} />);
const input = screen.getByPlaceholderText('Search...');
const button = screen.getByRole('button', { name: /search/i });
await user.type(input, 'react');
await user.click(button);
expect(onSearch).toHaveBeenCalledWith('react');
});
异步操作 #
waitFor #
javascript
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
test('loads user data', async () => {
render(<UserProfile userId={1} />);
// 等待加载完成
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
// 或使用 findBy
const userName = await screen.findByText('John Doe');
expect(userName).toBeInTheDocument();
});
异步组件 #
javascript
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
test('displays user list', async () => {
render(<UserList />);
// 等待加载状态消失
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
// 验证用户列表
const users = await screen.findAllByRole('listitem');
expect(users).toHaveLength(3);
});
Mock API #
javascript
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
jest.mock('./api', () => ({
fetchUser: jest.fn(() =>
Promise.resolve({ id: 1, name: 'John Doe' })
),
}));
test('fetches and displays user', async () => {
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
React 组件测试 #
简单组件 #
javascript
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('renders greeting', () => {
render(<Greeting name="John" />);
expect(screen.getByText('Hello, John!')).toBeInTheDocument();
});
带 Props 的组件 #
javascript
import { render, screen } from '@testing-library/react';
import Button from './Button';
describe('Button', () => {
test('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
test('renders disabled', () => {
render(<Button disabled>Disabled</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
test('handles click', async () => {
const handleClick = jest.fn();
const user = userEvent.setup();
render(<Button onClick={handleClick}>Click</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
带状态的组件 #
javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
test('counter functionality', async () => {
const user = userEvent.setup();
render(<Counter />);
const incrementButton = screen.getByRole('button', { name: '+' });
const decrementButton = screen.getByRole('button', { name: '-' });
const countDisplay = screen.getByTestId('count');
expect(countDisplay).toHaveTextContent('0');
await user.click(incrementButton);
expect(countDisplay).toHaveTextContent('1');
await user.click(incrementButton);
expect(countDisplay).toHaveTextContent('2');
await user.click(decrementButton);
expect(countDisplay).toHaveTextContent('1');
});
带表单的组件 #
javascript
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ContactForm from './ContactForm';
test('form validation and submission', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<ContactForm onSubmit={onSubmit} />);
// 提交空表单
await user.click(screen.getByRole('button', { name: /submit/i }));
// 验证错误消息
expect(screen.getByText('Name is required')).toBeInTheDocument();
expect(screen.getByText('Email is required')).toBeInTheDocument();
// 填写表单
await user.type(screen.getByLabelText('Name'), 'John Doe');
await user.type(screen.getByLabelText('Email'), 'john@example.com');
await user.type(screen.getByLabelText('Message'), 'Hello!');
// 提交表单
await user.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com',
message: 'Hello!',
});
});
});
Vue 组件测试 #
基本测试 #
javascript
import { mount } from '@vue/test-utils';
import Button from './Button.vue';
test('renders button', () => {
const wrapper = mount(Button, {
slots: {
default: 'Click me',
},
});
expect(wrapper.text()).toContain('Click me');
});
Props 测试 #
javascript
import { mount } from '@vue/test-utils';
import UserCard from './UserCard.vue';
test('renders user info', () => {
const wrapper = mount(UserCard, {
props: {
user: {
id: 1,
name: 'John Doe',
email: 'john@example.com',
},
},
});
expect(wrapper.text()).toContain('John Doe');
expect(wrapper.text()).toContain('john@example.com');
});
事件测试 #
javascript
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
test('counter increments', async () => {
const wrapper = mount(Counter);
expect(wrapper.vm.count).toBe(0);
await wrapper.find('button').trigger('click');
expect(wrapper.vm.count).toBe(1);
});
最佳实践 #
1. 使用可访问性查询 #
javascript
// ✅ 好的做法 - 使用角色查询
screen.getByRole('button', { name: /submit/i });
// ❌ 不好的做法 - 使用测试 ID
screen.getByTestId('submit-button');
2. 模拟用户行为 #
javascript
// ✅ 好的做法 - 使用 userEvent
await user.click(button);
await user.type(input, 'text');
// ❌ 不好的做法 - 使用 fireEvent
fireEvent.click(button);
3. 测试行为而非实现 #
javascript
// ✅ 好的做法 - 测试用户看到的内容
test('shows error message', async () => {
render(<Form />);
await user.click(submitButton);
expect(screen.getByText('Name is required')).toBeInTheDocument();
});
// ❌ 不好的做法 - 测试内部状态
test('sets error state', async () => {
const { result } = renderHook(() => useForm());
expect(result.current.errors.name).toBe('Name is required');
});
4. 避免实现细节 #
javascript
// ✅ 好的做法
expect(screen.getByRole('alert')).toHaveTextContent('Error');
// ❌ 不好的做法
expect(container.querySelector('.error-message span').textContent).toBe('Error');
常见问题 #
元素找不到 #
javascript
// 问题:元素在异步操作后出现
// 解决方案:使用 findBy
const element = await screen.findByText('Loaded');
多个匹配元素 #
javascript
// 问题:getByText 找到多个元素
// 解决方案:使用 getAllBy
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(3);
事件未触发 #
javascript
// 问题:fireEvent 没有触发更新
// 解决方案:使用 userEvent 或等待更新
await user.click(button);
await waitFor(() => {
expect(screen.getByText('Updated')).toBeInTheDocument();
});
下一步 #
现在你已经掌握了 Jest DOM 测试,接下来学习 代码覆盖率 学习如何衡量测试质量!
最后更新:2026-03-28