name: 测试代码 description: 为功能编写自动化测试,根据验收标准验证功能,并确保代码覆盖率。在编写测试代码、验证功能或向现有代码添加测试覆盖时使用。
测试代码
核心工作流程
测试编写遵循系统化方法:确定范围、理解模式、映射需求、编写测试、验证覆盖。
1. 确定测试范围
阅读项目文档:
docs/user-stories/US-###-*.md用于测试的验收标准docs/feature-spec/F-##-*.md用于技术需求docs/api-contracts.yaml用于API规范- 现有测试文件以理解模式
选择所需的测试类型:
- 单元测试: 单个函数、纯逻辑、实用工具
- 集成测试: 多个组件协同工作、API端点
- 组件测试: UI组件、用户交互
- 端到端测试: 完整用户流程、关键路径
- 合约测试: API请求/响应验证
- 性能测试: 负载、压力、基准测试
2. 理解现有模式
调查当前测试方法:
- 测试框架(Jest、Vitest、Pytest等)
- 模拟模式和实用工具
- 测试数据夹具和设置/清理
- 断言风格
如果不熟悉测试结构,请使用 code-finder 代理。
3. 将测试映射到需求
将3-5个验收标准转换为跨测试类型的具体测试用例:
示例映射:
## 用户故事:US-101 用户登录
### 测试用例
1. **单元:认证服务**
- validateCredentials() 对有效邮箱/密码返回true
- validateCredentials() 对无效密码返回false
- checkAccountStatus() 检测锁定账户
2. **集成:登录端点**
- POST /api/login 使用有效凭据返回200 + 令牌
- POST /api/login 使用无效凭据返回401 + 错误
- POST /api/login 使用锁定账户返回403
3. **组件:登录表单**
- 提交表单调用登录API
- 错误消息在401响应时显示
- 成功重定向到 /dashboard
4. **端到端:完整登录流程**
- 用户输入凭据 → 提交 → 看到仪表板
- 用户输入错误密码 → 看到错误 → 重试成功
4. 编写测试
单元测试结构:
describe('AuthService', () => {
describe('validateCredentials', () => {
it('对有效邮箱和密码返回true', async () => {
const result = await authService.validateCredentials(
'user@example.com',
'ValidPass123'
);
expect(result).toBe(true);
});
it('对无效密码返回false', async () => {
const result = await authService.validateCredentials(
'user@example.com',
'WrongPassword'
);
expect(result).toBe(false);
});
});
});
集成测试结构:
describe('POST /api/auth/login', () => {
beforeEach(async () => {
await resetTestDatabase();
await createTestUser({
email: 'test@example.com',
password: 'Test123!'
});
});
it('对有效凭据返回200和令牌', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'Test123!' });
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('token');
expect(response.body.token).toMatch(/^eyJ/); // JWT格式
});
it('对无效密码返回401', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'WrongPassword' });
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid credentials');
});
});
组件测试结构:
describe('LoginForm', () => {
it('用有效数据提交表单', async () => {
const mockLogin = jest.fn().mockResolvedValue({ success: true });
render(<LoginForm onLogin={mockLogin} />);
await userEvent.type(screen.getByLabelText(/email/i), 'user@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'Password123');
await userEvent.click(screen.getByRole('button', { name: /log in/i }));
expect(mockLogin).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'Password123'
});
});
it('在API失败时显示错误消息', async () => {
const mockLogin = jest.fn().mockRejectedValue(new Error('Invalid credentials'));
render(<LoginForm onLogin={mockLogin} />);
await userEvent.type(screen.getByLabelText(/email/i), 'user@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'wrong');
await userEvent.click(screen.getByRole('button', { name: /log in/i }));
expect(await screen.findByText(/invalid credentials/i)).toBeInTheDocument();
});
});
端到端测试结构:
test('用户可以成功登录', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'Test123!');
await page.click('button:has-text("Log In")');
await page.waitForURL('/dashboard');
expect(page.url()).toContain('/dashboard');
});
5. 边缘情况与错误场景
包括边界条件和错误路径:
describe('边缘情况', () => {
it('优雅处理空邮箱', async () => {
await expect(
authService.validateCredentials('', 'password')
).rejects.toThrow('Email is required');
});
it('处理极长密码', async () => {
const longPassword = 'a'.repeat(10000);
await expect(
authService.validateCredentials('user@example.com', longPassword)
).rejects.toThrow('Password too long');
});
it('处理网络超时', async () => {
jest.spyOn(global, 'fetch').mockImplementation(
() => new Promise((resolve) => setTimeout(resolve, 10000))
);
await expect(
authService.login('user@example.com', 'pass')
).rejects.toThrow('Request timeout');
});
});
始终包括的边缘情况:
- 空/空输入
- 最小/最大值
- 无效格式
- 网络故障
- API错误(4xx、5xx)
- 超时条件
- 并发操作
6. 测试数据与夹具
创建可重用的测试夹具:
// tests/fixtures/users.ts
export const validUser = {
email: 'test@example.com',
password: 'Test123!',
name: 'Test User'
};
export const invalidUsers = {
noEmail: { password: 'Test123!' },
noPassword: { email: 'test@example.com' },
invalidEmail: { email: 'not-an-email', password: 'Test123!' },
weakPassword: { email: 'test@example.com', password: '123' }
};
// 在测试中使用
import { validUser, invalidUsers } from './fixtures/users';
it('验证用户数据', () => {
expect(validate(validUser)).toBe(true);
expect(validate(invalidUsers.noEmail)).toBe(false);
});
7. 并行测试实施
当测试独立时(不同模块、不同测试类型),启动并行代理:
模式1:基于层
- 代理1:服务/实用工具的单元测试
- 代理2:API端点的集成测试
- 代理3:UI的组件测试
- 代理4:关键流程的端到端测试
模式2:基于功能
- 代理1:功能A的所有测试
- 代理2:功能B的所有测试
- 代理3:功能C的所有测试
模式3:基于类型
- 代理1:所有单元测试
- 代理2:所有集成测试
- 代理3:所有端到端测试
8. 运行与验证测试
执行测试套件:
# 单元测试
npm test -- --coverage
# 集成测试
npm run test:integration
# 端到端测试
npm run test:e2e
# 所有测试
npm run test:all
验证覆盖:
- 目标 >80% 代码覆盖率
- 关键路径100%覆盖
- 所有验收标准有测试
- 所有错误场景已测试
质量检查清单
覆盖:
- [ ] 用户故事的所有验收标准已测试
- [ ] 快乐路径已覆盖
- [ ] 包括边缘情况
- [ ] 错误场景已测试
- [ ] 边界条件已验证
结构:
- [ ] 测试遵循现有模式
- [ ] 清晰的测试描述
- [ ] 适当的设置/清理
- [ ] 无随机测试(结果一致)
- [ ] 测试是隔离的(无相互依赖)
数据:
- [ ] 测试夹具可重用
- [ ] 数据库正确播种/重置
- [ ] 模拟使用适当
- [ ] 无硬编码测试数据在生产中
集成:
- [ ] 测试在CI/CD中运行
- [ ] 覆盖阈值强制执行
- [ ] 快速反馈(快速测试)
- [ ] 清晰的失败消息