名称: jest-testing-patterns 用户可调用: false 描述: 当需要Jest测试模式时使用,包括单元测试、模拟、间谍、快照和断言技术,以实现全面的测试覆盖。 允许工具: [读取, 写入, 编辑, Bash, Glob, Grep]
Jest测试模式
掌握Jest测试模式,包括单元测试、模拟、间谍、快照和断言技术,以实现全面的测试覆盖。这个技能涵盖了使用Jest编写有效、可维护测试的基本模式和实践。
基本测试结构
测试套件组织
describe('Calculator', () => {
describe('add', () => {
it('应该添加两个正数', () => {
expect(add(2, 3)).toBe(5);
});
it('应该添加负数', () => {
expect(add(-2, -3)).toBe(-5);
});
it('应该处理零', () => {
expect(add(0, 5)).toBe(5);
});
});
describe('subtract', () => {
it('应该减去两个数', () => {
expect(subtract(5, 3)).toBe(2);
});
});
});
设置和清理
describe('数据库操作', () => {
let db;
// 在所有测试之前运行一次
beforeAll(async () => {
db = await initializeDatabase();
});
// 在所有测试之后运行一次
afterAll(async () => {
await db.close();
});
// 在每个测试之前运行
beforeEach(() => {
db.clear();
});
// 在每个测试之后运行
afterEach(() => {
db.resetMocks();
});
it('应该插入一条记录', async () => {
const result = await db.insert({ name: 'John' });
expect(result.id).toBeDefined();
});
it('应该找到一条记录', async () => {
await db.insert({ id: 1, name: 'John' });
const result = await db.findById(1);
expect(result.name).toBe('John');
});
});
匹配器和断言
常见匹配器
describe('匹配器', () => {
it('应该测试相等性', () => {
expect(2 + 2).toBe(4); // 严格相等
expect({ a: 1 }).toEqual({ a: 1 }); // 深度相等
expect([1, 2, 3]).toStrictEqual([1, 2, 3]); // 严格深度相等
});
it('应该测试真值性', () => {
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('value').toBeDefined();
});
it('应该测试数字', () => {
expect(4).toBeGreaterThan(3);
expect(4).toBeGreaterThanOrEqual(4);
expect(3).toBeLessThan(4);
expect(3).toBeLessThanOrEqual(3);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
});
it('应该测试字符串', () => {
expect('team').not.toMatch(/I/);
expect('Christoph').toMatch(/stop/);
expect('hello world').toContain('world');
});
it('应该测试数组和可迭代对象', () => {
const list = ['apple', 'banana', 'cherry'];
expect(list).toContain('banana');
expect(list).toHaveLength(3);
expect(new Set(list)).toContain('apple');
});
it('应该测试对象', () => {
expect({ a: 1, b: 2 }).toHaveProperty('a');
expect({ a: 1, b: 2 }).toHaveProperty('a', 1);
expect({ a: { b: { c: 1 } } }).toHaveProperty('a.b.c', 1);
});
it('应该测试异常', () => {
expect(() => {
throw new Error('error');
}).toThrow();
expect(() => {
throw new Error('Invalid input');
}).toThrow('Invalid input');
expect(() => {
throw new Error('Invalid input');
}).toThrow(/Invalid/);
});
});
异步断言
describe('异步测试', () => {
// 使用async/await
it('应该获取数据', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});
// 使用promises
it('应该使用promise获取数据', () => {
return fetchData().then(data => {
expect(data).toBeDefined();
});
});
// 测试promise拒绝
it('应该处理错误', async () => {
await expect(fetchInvalidData()).rejects.toThrow('Not found');
});
// 使用resolves/rejects
it('应该用数据解析', async () => {
await expect(fetchData()).resolves.toEqual({ id: 1 });
});
it('应该用错误拒绝', async () => {
await expect(fetchInvalidData()).rejects.toThrow();
});
});
模拟
函数模拟
describe('函数模拟', () => {
it('应该模拟一个函数', () => {
const mockFn = jest.fn();
mockFn('arg1', 'arg2');
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledTimes(1);
});
it('应该模拟返回值', () => {
const mockFn = jest.fn()
.mockReturnValue(42)
.mockReturnValueOnce(1)
.mockReturnValueOnce(2);
expect(mockFn()).toBe(1);
expect(mockFn()).toBe(2);
expect(mockFn()).toBe(42);
});
it('应该模拟异步函数', async () => {
const mockFn = jest.fn()
.mockResolvedValue('success')
.mockResolvedValueOnce('first call');
expect(await mockFn()).toBe('first call');
expect(await mockFn()).toBe('success');
});
it('应该模拟实现', () => {
const mockFn = jest.fn((a, b) => a + b);
expect(mockFn(1, 2)).toBe(3);
mockFn.mockImplementation((a, b) => a * b);
expect(mockFn(2, 3)).toBe(6);
});
});
模块模拟
// __mocks__/axios.js
export default {
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} }))
};
// userService.test.js
import axios from 'axios';
import { getUser, createUser } from './userService';
jest.mock('axios');
describe('用户服务', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('应该获取用户', async () => {
const mockUser = { id: 1, name: 'John' };
axios.get.mockResolvedValue({ data: mockUser });
const user = await getUser(1);
expect(axios.get).toHaveBeenCalledWith('/users/1');
expect(user).toEqual(mockUser);
});
it('应该创建用户', async () => {
const newUser = { name: 'Jane' };
const createdUser = { id: 2, name: 'Jane' };
axios.post.mockResolvedValue({ data: createdUser });
const user = await createUser(newUser);
expect(axios.post).toHaveBeenCalledWith('/users', newUser);
expect(user).toEqual(createdUser);
});
});
部分模拟
// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
// calculator.test.js
import * as utils from './utils';
jest.mock('./utils', () => ({
...jest.requireActual('./utils'),
multiply: jest.fn()
}));
describe('计算器', () => {
it('应该使用真实的add函数', () => {
expect(utils.add(2, 3)).toBe(5);
});
it('应该使用模拟的multiply函数', () => {
utils.multiply.mockReturnValue(10);
expect(utils.multiply(2, 3)).toBe(10);
expect(utils.multiply).toHaveBeenCalledWith(2, 3);
});
});
间谍
监视方法
describe('间谍', () => {
it('应该监视对象方法', () => {
const calculator = {
add: (a, b) => a + b
};
const spy = jest.spyOn(calculator, 'add');
calculator.add(2, 3);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(2, 3);
expect(spy).toHaveReturnedWith(5);
spy.mockRestore();
});
it('应该监视并模拟实现', () => {
const logger = {
log: (message) => console.log(message)
};
const spy = jest.spyOn(logger, 'log').mockImplementation(() => {});
logger.log('test');
expect(spy).toHaveBeenCalledWith('test');
spy.mockRestore();
});
it('应该监视getter', () => {
const obj = {
get value() {
return 42;
}
};
const spy = jest.spyOn(obj, 'value', 'get').mockReturnValue(100);
expect(obj.value).toBe(100);
spy.mockRestore();
});
});
监视全局函数
describe('全局间谍', () => {
it('应该监视console.log', () => {
const spy = jest.spyOn(console, 'log').mockImplementation();
console.log('test message');
expect(spy).toHaveBeenCalledWith('test message');
spy.mockRestore();
});
it('应该监视Date', () => {
const mockDate = new Date('2024-01-01');
const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
expect(new Date()).toBe(mockDate);
spy.mockRestore();
});
});
快照测试
基本快照
import { render } from '@testing-library/react';
import Button from './Button';
describe('按钮组件', () => {
it('应该匹配快照', () => {
const { container } = render(<Button label="Click me" />);
expect(container.firstChild).toMatchSnapshot();
});
it('应该匹配内联快照', () => {
const user = {
name: 'John Doe',
age: 30
};
expect(user).toMatchInlineSnapshot(`
{
"age": 30,
"name": "John Doe",
}
`);
});
});
属性匹配器
describe('带有动态数据的快照', () => {
it('应该使用属性匹配器匹配快照', () => {
const user = {
id: generateId(),
createdAt: new Date(),
name: 'John Doe'
};
expect(user).toMatchSnapshot({
id: expect.any(String),
createdAt: expect.any(Date)
});
});
});
自定义序列化器
// custom-serializer.js
module.exports = {
test(val) {
return val && val.hasOwnProperty('_reactInternalFiber');
},
serialize(val, config, indentation, depth, refs, printer) {
return `<ReactElement ${val.type} />`;
}
};
// jest.config.js
module.exports = {
snapshotSerializers: ['./custom-serializer.js']
};
测试模式
测试React组件
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Form from './Form';
describe('表单组件', () => {
it('应该渲染表单字段', () => {
render(<Form />);
expect(screen.getByLabelText('Name')).toBeInTheDocument();
expect(screen.getByLabelText('Email')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
});
it('应该处理用户输入', async () => {
const user = userEvent.setup();
render(<Form />);
const nameInput = screen.getByLabelText('Name');
await user.type(nameInput, 'John Doe');
expect(nameInput).toHaveValue('John Doe');
});
it('应该提交表单', async () => {
const onSubmit = jest.fn();
render(<Form onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText('Name'), 'John Doe');
await userEvent.type(screen.getByLabelText('Email'), 'john@example.com');
await userEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com'
});
});
});
it('应该显示验证错误', async () => {
render(<Form />);
const submitButton = screen.getByRole('button', { name: 'Submit' });
await userEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText('Name is required')).toBeInTheDocument();
});
});
});
测试异步操作
describe('异步操作', () => {
it('应该等待异步操作', async () => {
const promise = fetchData();
const data = await promise;
expect(data).toBeDefined();
});
it('应该使用假定时器', () => {
jest.useFakeTimers();
const callback = jest.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
jest.useRealTimers();
});
it('应该运行所有定时器', () => {
jest.useFakeTimers();
const callback1 = jest.fn();
const callback2 = jest.fn();
setTimeout(callback1, 1000);
setTimeout(callback2, 2000);
jest.runAllTimers();
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
jest.useRealTimers();
});
});
测试错误边界
import { render, screen } from '@testing-library/react';
import ErrorBoundary from './ErrorBoundary';
const ThrowError = () => {
throw new Error('Test error');
};
describe('错误边界', () => {
it('应该捕获错误并显示后备', () => {
// 为此测试抑制console.error
const spy = jest.spyOn(console, 'error').mockImplementation();
render(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
spy.mockRestore();
});
it('当没有错误时应渲染子元素', () => {
render(
<ErrorBoundary>
<div>Content</div>
</ErrorBoundary>
);
expect(screen.getByText('Content')).toBeInTheDocument();
});
});
最佳实践
- 编写描述性测试名称 - 使用清晰、具体的名称来解释测试验证的内容
- 遵循AAA模式 - 使用Arrange、Act、Assert部分结构测试以提高清晰度
- 测试行为,而不是实现 - 关注代码做什么,而不是如何做
- 使用适当的匹配器 - 选择最具体的匹配器以获得更好的错误消息
- 模拟外部依赖 - 隔离单元测试与外部服务和模块
- 测试后清理 - 使用afterEach重置模拟和清理副作用
- 避免测试私有方法 - 测试公共接口并信任实现细节
- 有效使用设置和清理钩子 - 利用beforeEach/afterEach进行通用设置
- 测试边缘情况和错误条件 - 不要只测试快乐路径
- 保持测试快速和隔离 - 每个测试应独立运行且快速
常见陷阱
- 测试间未清理模拟 - 导致测试污染和不稳定测试
- 测试实现细节 - 使测试脆弱且难以维护
- 过度使用快照 - 快照应补充,而不是替代,特定断言
- 忘记等待异步操作 - 导致误报和不稳定测试
- 未使用适当匹配器 - 通用匹配器提供差的错误消息
- 模拟过多 - 过度模拟降低测试信心和价值
- 将集成测试写作单元测试 - 混合关注点使测试缓慢和复杂
- 未测试错误场景 - 只测试快乐路径会遗漏错误
- 测试间共享状态 - 全局变量和单例导致测试相互依赖
- 测试名称不清晰 - 模糊的名称使难以理解测试失败
何时使用此技能
- 为函数和类编写单元测试
- 测试具有用户交互的React组件
- 模拟外部依赖和API
- 为UI组件创建快照测试
- 测试异步代码和promises
- 实现间谍以验证函数调用
- 测试错误处理和边缘情况
- 设置测试夹具和测试数据
- 调试失败测试和理解测试输出
- 重构测试以提高可维护性