设计测试
测试实施工作流程
复制此清单并跟踪进度:
测试实施进度:
- [ ] 第1步:确定测试内容
- [ ] 第2步:选择适当的测试类型
- [ ] 第3步:遵循模板编写测试
- [ ] 第4步:运行测试并验证通过
- [ ] 第5步:检查覆盖率是否达到目标
- [ ] 第6步:修复任何失败的测试
测试金字塔
应用测试金字塔以实现平衡覆盖:
/\
/ \ E2E测试(10%)
/----\ - 关键用户旅程
/ \ - 速度慢但全面
/--------\ 集成测试(20%)
/ \ - 组件交互
/------------\ - API契约
/ \ 单元测试(70%)
/________________\ - 快速,隔离
- 业务逻辑重点
框架选择
JavaScript/TypeScript
| 类型 | 推荐 | 替代 |
|---|---|---|
| 单元 | Vitest | Jest |
| 集成 | Vitest + MSW | Jest + SuperTest |
| E2E | Playwright | Cypress |
| 组件 | 测试库 | Enzyme |
Python
| 类型 | 推荐 | 替代 |
|---|---|---|
| 单元 | pytest | unittest |
| 集成 | pytest + httpx | pytest + requests |
| E2E | Playwright | Selenium |
| API | pytest + FastAPI TestClient | - |
Go
| 类型 | 推荐 |
|---|---|
| 单元 | testing + testify |
| 集成 | testing + httptest |
| E2E | testing + chromedp |
测试结构模板
单元测试
describe('【单元】组件名称', () => {
describe('方法名称', () => {
it('应该在[条件]时[预期行为]', () => {
// 安排
const input = createTestInput();
// 行动
const result = 方法名称(input);
// 断言
expect(result).toEqual(expectedOutput);
});
it('在[无效条件]时应抛出错误', () => {
expect(() => 方法名称(invalidInput)).toThrow(ExpectedError);
});
});
});
集成测试
describe('【集成】API /users', () => {
beforeAll(async () => {
await setupTestDatabase();
});
afterAll(async () => {
await teardownTestDatabase();
});
it('应该创建用户并返回201', async () => {
const response = await request(app)
.post('/users')
.send({ name: 'Test', email: 'test@example.com' });
expect(response.status).toBe(201);
expect(response.body.id).toBeDefined();
});
});
E2E测试
describe('【E2E】用户注册流程', () => {
it('应该成功完成注册', async ({ page }) => {
await page.goto('/register');
await page.fill('[data-testid="email"]', 'new@example.com');
await page.fill('[data-testid="password"]', 'SecurePass123!');
await page.click('[data-testid="submit"]');
await expect(page.locator('.welcome-message')).toBeVisible();
await expect(page).toHaveURL('/dashboard');
});
});
覆盖率策略
覆盖内容
- 业务逻辑(100%)
- 边缘情况和错误处理(90%+)
- API契约(100%)
- 关键用户路径(E2E)
- UI组件(快照+交互)
- 第三方库内部(不)
- 简单getter/setter(不)
覆盖率阈值
{
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
},
"src/core/": {
"branches": 95,
"functions": 95
}
}
}
测试数据管理
工厂/构建器
// factories/user.js
export const userFactory = (overrides = {}) => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
createdAt: new Date(),
...overrides,
});
// 使用
const admin = userFactory({ role: 'admin' });
固定装置
// fixtures/users.json
{
"validUser": { "name": "Test", "email": "test@example.com" },
"invalidUser": { "name": "", "email": "invalid" }
}
模拟策略
何时模拟
- 外部API和服务
- 单元测试中的数据库
- 确定性的时间/日期
- 随机值
- 内部模块(通常)
- 被测试的代码
模拟示例
// 使用MSW模拟API
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'John' },
]);
}),
];
// 时间模拟
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-01-01'));
测试验证循环
编写测试后,运行此验证:
测试验证:
- [ ] 所有测试通过:`npm test`
- [ ] 覆盖率满足阈值:`npm test -- --coverage`
- [ ] 无不稳定测试(多次运行)
- [ ] 测试独立(顺序不重要)
- [ ] 测试名称清晰描述行为
如果任何测试失败,请在继续之前修复它们。如果覆盖率低于目标,请为未覆盖的代码路径添加更多测试。
# 运行测试
npm test
# 带覆盖率运行
npm test -- --coverage
# 运行特定测试文件
npm test -- path/to/test.spec.ts
# 开发期间在监视模式下运行
npm test -- --watch