学习目标
- 掌握Jest测试框架
- 学会使用React Testing Library
- 理解单元测试和集成测试
- 掌握Mock和测试工具
- 构建完整的测试套件
学习时间安排
总时长:8-9小时
- 测试环境搭建:1小时
- Jest基础:2小时
- React Testing Library:2.5小时
- 高级测试技巧:1.5小时
- 实践项目:2-3小时
第一部分:测试环境搭建 (1小时)
1.1 安装测试依赖
安装测试工具
bash
# 安装Jest和React Testing Library
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
# 安装Jest相关依赖
npm install --save-dev jest jest-environment-jsdom @types/jest
# 安装测试覆盖率工具
npm install --save-dev @jest/coverage
# 如果使用TypeScript
npm install --save-dev @types/testing-library__react @types/testing-library__jest-dom
1.2 Jest配置
Jest配置文件
javascript
// jest.config.js
module.exports = {
// 测试环境
testEnvironment: 'jsdom',
// 设置文件,在每个测试文件运行前执行
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
// 模块文件扩展名
moduleFileExtensions: ['js', 'jsx', 'json', 'ts', 'tsx'],
// 模块名称映射
moduleNameMapper: {
// 处理CSS模块
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
// 处理静态资源
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js'
},
// 转换配置
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest'
},
// 测试文件匹配模式
testMatch: [
'**/__tests__/**/*.(js|jsx|ts|tsx)',
'**/*.(test|spec).(js|jsx|ts|tsx)'
],
// 收集覆盖率的文件
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/reportWebVitals.js'
],
// 覆盖率阈值
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70
}
},
// 覆盖率报告格式
coverageReporters: ['text', 'lcov', 'html'],
// 清除模拟
clearMocks: true,
// 恢复模拟
restoreMocks: true
};
测试设置文件
javascript
// src/setupTests.js
// 导入Jest DOM匹配器
// 这些匹配器扩展了Jest的断言功能,专门用于DOM测试
import '@testing-library/jest-dom';
// 配置测试环境
// 设置全局测试配置
// Mock window.matchMedia
// 某些组件可能使用matchMedia API,需要在测试环境中模拟
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Mock IntersectionObserver
// 某些组件可能使用IntersectionObserver API,需要在测试环境中模拟
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
takeRecords() {
return [];
}
unobserve() {}
};
// Mock ResizeObserver
// 某些组件可能使用ResizeObserver API,需要在测试环境中模拟
global.ResizeObserver = class ResizeObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
};
// 配置测试超时时间
jest.setTimeout(10000);
// 全局测试工具函数
global.testUtils = {
// 等待异步操作完成
waitFor: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
// 创建模拟用户
createMockUser: (overrides = {}) => ({
id: 1,
name: 'Test User',
email: 'test@example.com',
...overrides
}),
// 创建模拟数据
createMockData: (count = 10) => {
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: `Item ${i + 1}`,
value: Math.random() * 100
}));
}
};
第二部分:Jest基础 (2小时)
2.1 Jest基础语法
基础测试示例
javascript
// src/utils/__tests__/math.test.js
// 导入要测试的函数
import { add, subtract, multiply, divide } from '../math';
// describe函数用于组织相关测试
// 第一个参数是测试套件的描述
// 第二个参数是包含测试用例的函数
describe('Math utilities', () => {
// test或it函数用于定义单个测试用例
// 第一个参数是测试用例的描述
// 第二个参数是测试函数
test('add function should add two numbers correctly', () => {
// expect函数用于创建断言
// toBe是匹配器,用于检查值是否严格相等
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
test('subtract function should subtract two numbers correctly', () => {
expect(subtract(5, 3)).toBe(2);
expect(subtract(0, 5)).toBe(-5);
expect(subtract(10, 10)).toBe(0);
});
test('multiply function should multiply two numbers correctly', () => {
expect(multiply(2, 3)).toBe(6);
expect(multiply(-2, 3)).toBe(-6);
expect(multiply(0, 5)).toBe(0);
});
test('divide function should divide two numbers correctly', () => {
expect(divide(6, 3)).toBe(2);
expect(divide(10, 2)).toBe(5);
expect(divide(-6, 3)).toBe(-2);
});
test('divide function should throw error when dividing by zero', () => {
// toThrow匹配器用于检查函数是否抛出错误
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
常用匹配器
javascript
// src/utils/__tests__/matchers.test.js
describe('Jest Matchers', () => {
test('equality matchers', () => {
// toBe用于严格相等比较(使用Object.is)
expect(2 + 2).toBe(4);
// toEqual用于深度相等比较(递归比较对象属性)
expect({ name: 'John', age: 30 }).toEqual({ name: 'John', age: 30 });
// not用于取反
expect(2 + 2).not.toBe(5);
});
test('truthiness matchers', () => {
// toBeTruthy检查值是否为真值
expect(true).toBeTruthy();
expect(1).toBeTruthy();
expect('hello').toBeTruthy();
// toBeFalsy检查值是否为假值
expect(false).toBeFalsy();
expect(0).toBeFalsy();
expect('').toBeFalsy();
expect(null).toBeFalsy();
expect(undefined).toBeFalsy();
});
test('number matchers', () => {
const value = 2 + 2;
// toBeGreaterThan检查值是否大于
expect(value).toBeGreaterThan(3);
// toBeGreaterThanOrEqual检查值是否大于等于
expect(value).toBeGreaterThanOrEqual(4);
// toBeLessThan检查值是否小于
expect(value).toBeLessThan(5);
// toBeLessThanOrEqual检查值是否小于等于
expect(value).toBeLessThanOrEqual(4);
// toBeCloseTo用于浮点数比较(避免精度问题)
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
test('string matchers', () => {
const str = 'Hello World';
// toMatch用于正则表达式匹配
expect(str).toMatch(/Hello/);
expect(str).toMatch('World');
// toContain检查字符串是否包含子字符串
expect(str).toContain('Hello');
});
test('array matchers', () => {
const arr = [1, 2, 3, 4, 5];
// toContain检查数组是否包含元素
expect(arr).toContain(3);
expect(arr).not.toContain(6);
// toHaveLength检查数组长度
expect(arr).toHaveLength(5);
});
test('object matchers', () => {
const obj = {
name: 'John',
age: 30,
email: 'john@example.com'
};
// toHaveProperty检查对象是否具有属性
expect(obj).toHaveProperty('name');
expect(obj).toHaveProperty('age', 30);
// toMatchObject检查对象是否匹配部分属性
expect(obj).toMatchObject({ name: 'John', age: 30 });
});
test('exception matchers', () => {
// toThrow检查函数是否抛出错误
expect(() => {
throw new Error('Something went wrong');
}).toThrow();
expect(() => {
throw new Error('Something went wrong');
}).toThrow('Something went wrong');
expect(() => {
throw new Error('Something went wrong');
}).toThrow(Error);
});
});
2.2 异步测试
异步测试示例
javascript
// src/utils/__tests__/async.test.js
// 导入要测试的异步函数
import { fetchData, fetchUser, processData } from '../async';
describe('Async functions', () => {
// 测试Promise
test('fetchData should return data', () => {
// 返回Promise,Jest会等待Promise解决
return fetchData().then(data => {
expect(data).toBeDefined();
expect(data).toHaveProperty('id');
expect(data).toHaveProperty('name');
});
});
// 使用async/await测试异步函数
test('fetchUser should return user data', async () => {
const user = await fetchUser(1);
expect(user).toBeDefined();
expect(user.id).toBe(1);
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('email');
});
// 测试异步错误
test('fetchUser should throw error for invalid id', async () => {
// 使用rejects匹配器测试Promise拒绝
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
// 测试多个异步操作
test('processData should process multiple items', async () => {
const items = [1, 2, 3];
const results = await Promise.all(
items.map(item => processData(item))
);
expect(results).toHaveLength(3);
results.forEach(result => {
expect(result).toBeDefined();
expect(result).toHaveProperty('processed');
});
});
// 使用waitFor测试异步更新
test('should update state asynchronously', async () => {
const { waitFor } = require('@testing-library/react');
let state = { value: 0 };
// 模拟异步更新
setTimeout(() => {
state.value = 100;
}, 100);
// 等待状态更新
await waitFor(() => {
expect(state.value).toBe(100);
});
});
});
第三部分:React Testing Library (2.5小时)
3.1 组件渲染测试
基础组件测试
javascript
// src/components/__tests__/Button.test.js
// 导入React
import React from 'react';
// 导入测试工具
import { render, screen, fireEvent } from '@testing-library/react';
// 导入要测试的组件
import Button from '../Button';
// describe函数组织测试套件
describe('Button Component', () => {
// 测试基本渲染
test('should render button with text', () => {
// render函数渲染组件到DOM
render(<Button>Click me</Button>);
// screen对象提供查询DOM的方法
// getByText查找包含指定文本的元素
const button = screen.getByText('Click me');
// 断言按钮存在
expect(button).toBeInTheDocument();
});
// 测试按钮点击事件
test('should call onClick when clicked', () => {
// 创建模拟函数
const handleClick = jest.fn();
// 渲染组件并传递onClick处理函数
render(<Button onClick={handleClick}>Click me</Button>);
// 查找按钮元素
const button = screen.getByText('Click me');
// fireEvent模拟用户事件
fireEvent.click(button);
// 断言函数被调用
expect(handleClick).toHaveBeenCalledTimes(1);
});
// 测试按钮禁用状态
test('should be disabled when disabled prop is true', () => {
render(<Button disabled>Disabled Button</Button>);
const button = screen.getByText('Disabled Button');
// 断言按钮被禁用
expect(button).toBeDisabled();
});
// 测试按钮样式类
test('should apply custom className', () => {
render(<Button className="custom-class">Button</Button>);
const button = screen.getByText('Button');
// 断言按钮具有自定义类名
expect(button).toHaveClass('custom-class');
});
// 测试按钮类型
test('should render with correct type', () => {
render(<Button type="submit">Submit</Button>);
const button = screen.getByText('Submit');
// 断言按钮类型正确
expect(button).toHaveAttribute('type', 'submit');
});
});
3.2 表单测试
表单组件测试
javascript
// src/components/__tests__/Form.test.js
// 导入React
import React from 'react';
// 导入测试工具
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
// 导入用户事件库
import userEvent from '@testing-library/user-event';
// 导入要测试的组件
import Form from '../Form';
describe('Form Component', () => {
// 测试表单渲染
test('should render form with all fields', () => {
render(<Form />);
// 使用getByLabelText查找标签关联的输入框
expect(screen.getByLabelText('Name:')).toBeInTheDocument();
expect(screen.getByLabelText('Email:')).toBeInTheDocument();
expect(screen.getByLabelText('Message:')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
});
// 测试表单输入
test('should update input values when user types', async () => {
// 创建用户事件实例
const user = userEvent.setup();
render(<Form />);
// 获取输入框
const nameInput = screen.getByLabelText('Name:');
const emailInput = screen.getByLabelText('Email:');
const messageInput = screen.getByLabelText('Message:');
// 使用userEvent.type模拟用户输入
await user.type(nameInput, 'John Doe');
await user.type(emailInput, 'john@example.com');
await user.type(messageInput, 'Hello World');
// 断言输入值已更新
expect(nameInput).toHaveValue('John Doe');
expect(emailInput).toHaveValue('john@example.com');
expect(messageInput).toHaveValue('Hello World');
});
// 测试表单提交
test('should submit form with correct data', async () => {
const handleSubmit = jest.fn();
const user = userEvent.setup();
render(<Form onSubmit={handleSubmit} />);
// 填写表单
await user.type(screen.getByLabelText('Name:'), 'John Doe');
await user.type(screen.getByLabelText('Email:'), 'john@example.com');
await user.type(screen.getByLabelText('Message:'), 'Hello World');
// 提交表单
await user.click(screen.getByRole('button', { name: 'Submit' }));
// 等待异步操作完成
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com',
message: 'Hello World'
});
});
});
// 测试表单验证
test('should show validation errors for empty fields', async () => {
const user = userEvent.setup();
render(<Form />);
// 直接提交空表单
await user.click(screen.getByRole('button', { name: 'Submit' }));
// 等待验证错误显示
await waitFor(() => {
expect(screen.getByText('Name is required')).toBeInTheDocument();
expect(screen.getByText('Email is required')).toBeInTheDocument();
});
});
// 测试表单重置
test('should reset form after successful submission', async () => {
const handleSubmit = jest.fn().mockResolvedValue({ success: true });
const user = userEvent.setup();
render(<Form onSubmit={handleSubmit} />);
// 填写并提交表单
await user.type(screen.getByLabelText('Name:'), 'John Doe');
await user.type(screen.getByLabelText('Email:'), 'john@example.com');
await user.click(screen.getByRole('button', { name: 'Submit' }));
// 等待表单重置
await waitFor(() => {
expect(screen.getByLabelText('Name:')).toHaveValue('');
expect(screen.getByLabelText('Email:')).toHaveValue('');
});
});
});
3.3 Hooks测试
Hooks测试示例
javascript
// src/hooks/__tests__/useCounter.test.js
// 导入React和测试工具
import { renderHook, act } from '@testing-library/react';
// 导入要测试的Hook
import { useCounter } from '../useCounter';
describe('useCounter Hook', () => {
// 测试初始值
test('should initialize with default value', () => {
// renderHook用于测试自定义Hook
const { result } = renderHook(() => useCounter());
// result.current包含Hook的返回值
expect(result.current.count).toBe(0);
});
// 测试初始值设置
test('should initialize with custom value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
// 测试增加计数
test('should increment count', () => {
const { result } = renderHook(() => useCounter(0));
// act函数包装状态更新,确保更新完成后再断言
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(2);
});
// 测试减少计数
test('should decrement count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
// 测试重置计数
test('should reset count to initial value', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(12);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
// 测试设置值
test('should set count to specific value', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.setCount(100);
});
expect(result.current.count).toBe(100);
});
});
useEffect测试
javascript
// src/hooks/__tests__/useFetch.test.js
// 导入React和测试工具
import { renderHook, waitFor } from '@testing-library/react';
// 导入要测试的Hook
import { useFetch } from '../useFetch';
// Mock fetch API
global.fetch = jest.fn();
describe('useFetch Hook', () => {
// 在每个测试前清除mock
beforeEach(() => {
fetch.mockClear();
});
// 测试成功获取数据
test('should fetch data successfully', async () => {
const mockData = { id: 1, name: 'Test User' };
// Mock fetch返回成功响应
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockData
});
const { result } = renderHook(() => useFetch('/api/user/1'));
// 初始状态应该是加载中
expect(result.current.loading).toBe(true);
expect(result.current.data).toBe(null);
expect(result.current.error).toBe(null);
// 等待数据加载完成
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
// 断言数据正确
expect(result.current.data).toEqual(mockData);
expect(result.current.error).toBe(null);
});
// 测试获取数据失败
test('should handle fetch error', async () => {
const errorMessage = 'Failed to fetch';
// Mock fetch返回错误
fetch.mockRejectedValueOnce(new Error(errorMessage));
const { result } = renderHook(() => useFetch('/api/user/1'));
// 等待错误处理完成
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
// 断言错误状态
expect(result.current.data).toBe(null);
expect(result.current.error).toBe(errorMessage);
});
// 测试URL变化时重新获取
test('should refetch when URL changes', async () => {
const mockData1 = { id: 1, name: 'User 1' };
const mockData2 = { id: 2, name: 'User 2' };
fetch
.mockResolvedValueOnce({
ok: true,
json: async () => mockData1
})
.mockResolvedValueOnce({
ok: true,
json: async () => mockData2
});
const { result, rerender } = renderHook(
({ url }) => useFetch(url),
{ initialProps: { url: '/api/user/1' } }
);
// 等待第一次获取完成
await waitFor(() => {
expect(result.current.data).toEqual(mockData1);
});
// 更改URL
rerender({ url: '/api/user/2' });
// 等待第二次获取完成
await waitFor(() => {
expect(result.current.data).toEqual(mockData2);
});
// 断言fetch被调用了两次
expect(fetch).toHaveBeenCalledTimes(2);
});
});
第四部分:高级测试技巧 (1.5小时)
4.1 Mock和Spy
Mock函数示例
javascript
// src/utils/__tests__/mock.test.js
// 导入要测试的函数
import { processUserData, sendEmail, calculateTotal } from '../utils';
// Mock外部模块
jest.mock('../api', () => ({
fetchUser: jest.fn(),
updateUser: jest.fn()
}));
describe('Mock and Spy examples', () => {
// 测试Mock函数
test('should use mock function', () => {
// 创建Mock函数
const mockFn = jest.fn();
// 调用Mock函数
mockFn('arg1', 'arg2');
// 断言Mock函数被调用
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
});
// 测试Mock返回值
test('should mock function return value', () => {
const mockFn = jest.fn();
// 设置Mock返回值
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
// 设置多次调用的不同返回值
mockFn
.mockReturnValueOnce(10)
.mockReturnValueOnce(20)
.mockReturnValue(30);
expect(mockFn()).toBe(10);
expect(mockFn()).toBe(20);
expect(mockFn()).toBe(30);
expect(mockFn()).toBe(30);
});
// 测试Mock Promise
test('should mock async function', async () => {
const mockAsyncFn = jest.fn();
// 设置Mock返回Promise
mockAsyncFn.mockResolvedValue({ data: 'success' });
const result = await mockAsyncFn();
expect(result).toEqual({ data: 'success' });
});
// 测试Spy
test('should spy on object method', () => {
const obj = {
method: jest.fn()
};
// 创建Spy
const spy = jest.spyOn(obj, 'method');
// 调用方法
obj.method('test');
// 断言Spy被调用
expect(spy).toHaveBeenCalledWith('test');
// 恢复原始实现
spy.mockRestore();
});
// 测试模块Mock
test('should mock module', async () => {
const { fetchUser } = require('../api');
// 设置Mock返回值
fetchUser.mockResolvedValue({ id: 1, name: 'Test User' });
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'Test User' });
expect(fetchUser).toHaveBeenCalledWith(1);
});
});
4.2 测试工具函数
测试工具函数
javascript
// src/utils/__tests__/testUtils.js
// 测试工具函数集合
// 创建测试包装器
export function createTestWrapper({ providers = [], initialProps = {} } = {}) {
return function TestWrapper({ children }) {
return providers.reduce(
(acc, Provider) => <Provider>{acc}</Provider>,
children
);
};
}
// 等待元素出现
export async function waitForElement(selector, options = {}) {
const { timeout = 5000, interval = 100 } = options;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const element = document.querySelector(selector);
if (element) {
return element;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
throw new Error(`Element ${selector} not found within ${timeout}ms`);
}
// 模拟用户输入
export async function typeInInput(input, text) {
const user = userEvent.setup();
await user.clear(input);
await user.type(input, text);
}
// 模拟表单提交
export async function submitForm(form) {
const user = userEvent.setup();
const submitButton = form.querySelector('button[type="submit"]');
if (submitButton) {
await user.click(submitButton);
}
}
// 创建Mock数据
export function createMockUser(overrides = {}) {
return {
id: 1,
name: 'Test User',
email: 'test@example.com',
role: 'user',
...overrides
};
}
export function createMockPost(overrides = {}) {
return {
id: 1,
title: 'Test Post',
content: 'Test content',
author: 'Test Author',
createdAt: new Date().toISOString(),
...overrides
};
}
// 等待异步操作
export function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 测试辅助函数
export const testUtils = {
waitForElement,
typeInInput,
submitForm,
createMockUser,
createMockPost,
wait
};
第五部分:实践项目
项目:完整的测试套件
待办事项组件测试
javascript
// src/components/__tests__/TodoApp.test.js
// 导入React
import React from 'react';
// 导入测试工具
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
// 导入用户事件
import userEvent from '@testing-library/user-event';
// 导入要测试的组件
import TodoApp from '../TodoApp';
describe('TodoApp Component', () => {
// 测试初始渲染
test('should render todo app with empty list', () => {
render(<TodoApp />);
// 断言主要元素存在
expect(screen.getByText('Todo App')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Add a new todo')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Add' })).toBeInTheDocument();
});
// 测试添加待办事项
test('should add new todo item', async () => {
const user = userEvent.setup();
render(<TodoApp />);
// 获取输入框和按钮
const input = screen.getByPlaceholderText('Add a new todo');
const addButton = screen.getByRole('button', { name: 'Add' });
// 输入文本并点击添加
await user.type(input, 'Buy groceries');
await user.click(addButton);
// 断言新待办事项已添加
expect(screen.getByText('Buy groceries')).toBeInTheDocument();
expect(input).toHaveValue('');
});
// 测试完成待办事项
test('should mark todo as completed', async () => {
const user = userEvent.setup();
render(<TodoApp />);
// 添加待办事项
const input = screen.getByPlaceholderText('Add a new todo');
await user.type(input, 'Test todo');
await user.click(screen.getByRole('button', { name: 'Add' }));
// 查找复选框并点击
const checkbox = screen.getByRole('checkbox');
await user.click(checkbox);
// 断言待办事项已完成
expect(checkbox).toBeChecked();
const todoItem = screen.getByText('Test todo');
expect(todoItem).toHaveClass('completed');
});
// 测试删除待办事项
test('should delete todo item', async () => {
const user = userEvent.setup();
render(<TodoApp />);
// 添加待办事项
const input = screen.getByPlaceholderText('Add a new todo');
await user.type(input, 'Todo to delete');
await user.click(screen.getByRole('button', { name: 'Add' }));
// 查找删除按钮并点击
const deleteButton = screen.getByRole('button', { name: 'Delete' });
await user.click(deleteButton);
// 断言待办事项已删除
expect(screen.queryByText('Todo to delete')).not.toBeInTheDocument();
});
// 测试过滤待办事项
test('should filter todos by status', async () => {
const user = userEvent.setup();
render(<TodoApp />);
// 添加多个待办事项
const input = screen.getByPlaceholderText('Add a new todo');
await user.type(input, 'Todo 1');
await user.click(screen.getByRole('button', { name: 'Add' }));
await user.type(input, 'Todo 2');
await user.click(screen.getByRole('button', { name: 'Add' }));
// 完成第一个待办事项
const checkboxes = screen.getAllByRole('checkbox');
await user.click(checkboxes[0]);
// 切换到已完成过滤器
const filterSelect = screen.getByLabelText('Filter:');
await user.selectOptions(filterSelect, 'completed');
// 断言只显示已完成的待办事项
expect(screen.getByText('Todo 1')).toBeInTheDocument();
expect(screen.queryByText('Todo 2')).not.toBeInTheDocument();
});
// 测试空输入验证
test('should not add empty todo', async () => {
const user = userEvent.setup();
render(<TodoApp />);
const input = screen.getByPlaceholderText('Add a new todo');
const addButton = screen.getByRole('button', { name: 'Add' });
// 尝试添加空待办事项
await user.click(addButton);
// 断言没有添加空待办事项
const todoItems = screen.queryAllByRole('listitem');
expect(todoItems).toHaveLength(0);
});
});
用户管理组件测试
javascript
// src/components/__tests__/UserManagement.test.js
// 导入React
import React from 'react';
// 导入测试工具
import { render, screen, waitFor } from '@testing-library/react';
// 导入用户事件
import userEvent from '@testing-library/user-event';
// 导入Provider
import { Provider } from 'react-redux';
import { store } from '../../store/store';
// 导入要测试的组件
import UserManagement from '../UserManagement';
// 创建测试包装器
const TestWrapper = ({ children }) => (
<Provider store={store}>
{children}
</Provider>
);
describe('UserManagement Component', () => {
// 测试用户列表渲染
test('should render user list', async () => {
render(
<TestWrapper>
<UserManagement />
</TestWrapper>
);
// 等待用户列表加载
await waitFor(() => {
expect(screen.getByText('User Management')).toBeInTheDocument();
});
});
// 测试添加用户
test('should add new user', async () => {
const user = userEvent.setup();
render(
<TestWrapper>
<UserManagement />
</TestWrapper>
);
// 等待组件加载
await waitFor(() => {
expect(screen.getByText('Add User')).toBeInTheDocument();
});
// 点击添加用户按钮
await user.click(screen.getByText('Add User'));
// 填写表单
await user.type(screen.getByLabelText('Name:'), 'John Doe');
await user.type(screen.getByLabelText('Email:'), 'john@example.com');
// 提交表单
await user.click(screen.getByRole('button', { name: 'Submit' }));
// 等待用户添加完成
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
// 测试编辑用户
test('should edit existing user', async () => {
const user = userEvent.setup();
render(
<TestWrapper>
<UserManagement />
</TestWrapper>
);
// 等待用户列表加载
await waitFor(() => {
expect(screen.getByText('User Management')).toBeInTheDocument();
});
// 查找编辑按钮并点击
const editButtons = screen.getAllByRole('button', { name: 'Edit' });
if (editButtons.length > 0) {
await user.click(editButtons[0]);
// 修改名称
const nameInput = screen.getByLabelText('Name:');
await user.clear(nameInput);
await user.type(nameInput, 'Updated Name');
// 提交更改
await user.click(screen.getByRole('button', { name: 'Save' }));
// 等待更新完成
await waitFor(() => {
expect(screen.getByText('Updated Name')).toBeInTheDocument();
});
}
});
// 测试删除用户
test('should delete user', async () => {
const user = userEvent.setup();
render(
<TestWrapper>
<UserManagement />
</TestWrapper>
);
// 等待用户列表加载
await waitFor(() => {
expect(screen.getByText('User Management')).toBeInTheDocument();
});
// 查找删除按钮
const deleteButtons = screen.getAllByRole('button', { name: 'Delete' });
if (deleteButtons.length > 0) {
const userToDelete = screen.getAllByText(/User/)[0].textContent;
// 点击删除按钮
await user.click(deleteButtons[0]);
// 确认删除
const confirmButton = screen.getByRole('button', { name: 'Confirm' });
await user.click(confirmButton);
// 等待删除完成
await waitFor(() => {
expect(screen.queryByText(userToDelete)).not.toBeInTheDocument();
});
}
});
});
测试配置文件
javascript
// package.json测试脚本配置
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:verbose": "jest --verbose",
"test:update": "jest --updateSnapshot"
}
}
测试运行脚本
bash
# 运行所有测试
npm test
# 运行测试并监视文件变化
npm run test:watch
# 运行测试并生成覆盖率报告
npm run test:coverage
# 运行测试并显示详细信息
npm run test:verbose
# 更新快照
npm run test:update
练习题目
基础练习
- Jest基础练习
javascript
// 练习1:编写数学工具函数的测试
// 实现:加法、减法、乘法、除法函数的测试
// 包含:正常情况、边界情况、错误情况
// 练习2:编写字符串工具函数的测试
// 实现:字符串反转、大小写转换、去空格等函数
// 包含:各种输入情况的测试
- React组件测试练习
javascript
// 练习3:编写Button组件的完整测试
// 实现:渲染、点击、禁用、样式等测试
// 包含:各种props组合的测试
// 练习4:编写Form组件的完整测试
// 实现:输入、验证、提交、重置等测试
// 包含:表单验证逻辑的测试
进阶练习
- Hooks测试练习
javascript
// 练习5:编写自定义Hook的完整测试
// 实现:useCounter、useFetch、useLocalStorage等Hook
// 包含:各种使用场景的测试
// 练习6:编写复杂组件的集成测试
// 实现:包含多个子组件、状态管理、API调用的组件
// 包含:完整用户流程的测试
- E2E测试练习
javascript
// 练习7:使用Cypress或Playwright编写E2E测试
// 实现:完整的用户流程测试
// 包含:登录、数据操作、导航等流程