测试用例生成标准化模版(分包交付核心)
Token 建议输入 ≤15000 · 输出预留 12000claude-3.5-sonnetclaude-3.7-sonnetgpt-4o
从需求/接口契约生成结构化测试用例,覆盖功能/集成/契约/性能/安全,输出可执行测试脚本、测试数据、覆盖率门槛
提示词
你是测试架构师。任务:为分包项目生成全层级测试用例体系,作为交付验收的质量标尺。
约束:
- 测试金字塔:单元(70%) > 集成(20%) > E2E(10%)
- 来源追溯:每用例关联需求ID/契约端点/风险点
- 分类维度:功能、边界、异常、性能、安全、兼容性、数据迁移
- BDD 格式:Given/When/Then,业务可读
- 数据驱动:参数化测试数据、边界值、等价类划分
- 可执行:输出 Vitest/Playwright/Pytest 可直接跑通的代码
- 覆盖率门槛:语句/分支/函数/行 >= 80%
- CI 集成:矩阵测试、并行、失败重试、报告发布
- 输出:测试用例文档 + 可执行代码 + 测试数据工厂 + CI 配置
输出格式:
测试用例规范 v1.0:
追溯矩阵:
| 需求ID | 契约端点 | 测试用例ID | 类型 | 优先级 |
单元测试用例:
集成测试用例:
E2E 测试用例:
性能/安全测试用例:
// tests/unit/xxx.test.ts
// tests/integration/xxx.test.ts
// tests/e2e/xxx.spec.ts
.github/workflows/test.yml:
少样本示例
展示追溯矩阵、单元/集成/E2E 代码、性能/安全用例、CI 矩阵
输入
接口契约:POST /api/v1/auth/login(见 api-contract-design 模版)。需求:支持邮箱/手机号登录、记住我、设备指纹、失败锁定、刷新令牌。
输出
测试用例规范 v1.0 - 用户登录模块:
追溯矩阵:
| 需求ID | 契约端点 | 测试用例ID | 类型 | 优先级 |
|---|---|---|---|---|
| REQ-001 | POST /auth/login | TC-LOGIN-001 | 功能 | P0 |
| REQ-001 | POST /auth/login | TC-LOGIN-002 | 边界 | P0 |
| REQ-002 | POST /auth/login | TC-LOGIN-003 | 异常 | P0 |
| REQ-003 | POST /auth/refresh | TC-LOGIN-004 | 功能 | P0 |
| REQ-004 | POST /auth/login | TC-LOGIN-005 | 安全 | P1 |
单元测试用例 (Vitest):
TC-LOGIN-001: 邮箱密码登录成功:
- Given: 有效用户存在、密码正确、设备指纹有效
- When: 调用 loginService.login({ email, password, deviceFingerprint })
- Then: 返回 AuthResponse、accessToken 有效、refreshToken 有效、用户信息完整
TC-LOGIN-002: 手机号登录成功:
- Given: 用户绑定手机号、密码正确
- When: 调用 loginService.login({ phone, password, deviceFingerprint })
- Then: 同邮箱登录
TC-LOGIN-003: 密码错误触发失败计数:
- Given: 用户存在、密码错误
- When: 连续调用 5 次登录
- Then: 前 4 次返回 401、第 5 次返回 429(锁定)、Redis 记录失败次数
TC-LOGIN-004: 刷新令牌轮换:
- Given: 有效 refreshToken
- When: 调用 refreshToken()
- Then: 返回新 accessToken+refreshToken、旧 refreshToken 失效、黑名单记录
TC-LOGIN-005: 设备指纹风控:
- Given: 同一设备指纹 1 分钟内 20 次失败
- When: 第 21 次请求
- Then: 返回 429、设备指纹加入临时黑名单
// tests/unit/auth/login.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { LoginService } from '@/services/auth/login.service';
import { UserRepository } from '@/repositories/user.repository';
import { TokenService } from '@/services/auth/token.service';
import { RateLimiter } from '@/services/security/rate-limiter';
const mockUserRepo = vi.mocked(new UserRepository());
const mockTokenSvc = vi.mocked(new TokenService());
const mockRateLimiter = vi.mocked(new RateLimiter());
const loginSvc = new LoginService(mockUserRepo, mockTokenSvc, mockRateLimiter);
describe('LoginService', () => {
beforeEach(() => vi.clearAllMocks());
it('TC-LOGIN-001: email password login success', async () => {
const user = { id: '1', email: 'test@example.com', passwordHash: 'hashed', roles: ['user'] };
mockUserRepo.findByEmail.mockResolvedValue(user);
mockUserRepo.verifyPassword.mockResolvedValue(true);
mockTokenSvc.generatePair.mockResolvedValue({ accessToken: 'at', refreshToken: 'rt', expiresIn: 900 });
mockRateLimiter.check.mockResolvedValue({ allowed: true });
const result = await loginSvc.login({ email: 'test@example.com', password: 'pass', deviceFingerprint: 'fp1' });
expect(result).toMatchObject({ accessToken: 'at', refreshToken: 'rt', expiresIn: 900, tokenType: 'Bearer' });
expect(result.user.email).toBe('test@example.com');
});
it('TC-LOGIN-003: password error triggers lockout', async () => {
mockUserRepo.findByEmail.mockResolvedValue({ id: '1', passwordHash: 'h' });
mockUserRepo.verifyPassword.mockResolvedValue(false);
mockRateLimiter.check.mockResolvedValue({ allowed: true });
mockRateLimiter.increment.mockResolvedValue(undefined);
for (let i = 0; i < 4; i++) {
await expect(loginSvc.login({ email: 'a@b.com', password: 'wrong', deviceFingerprint: 'fp' }))
.rejects.toThrow('Invalid credentials');
}
mockRateLimiter.check.mockResolvedValueOnce({ allowed: false, retryAfter: 300 });
await expect(loginSvc.login({ email: 'a@b.com', password: 'wrong', deviceFingerprint: 'fp' }))
.rejects.toThrow('Too many attempts');
});
});
// tests/integration/auth/login.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import { app } from '@/app';
import { setupTestDb, teardownTestDb } from '../utils/db';
describe('POST /api/v1/auth/login (Integration)', () => {
beforeAll(async () => { await setupTestDb(); });
afterAll(async () => { await teardownTestDb(); });
it('TC-LOGIN-001: full stack login', async () => {
const res = await request(app)
.post('/api/v1/auth/login')
.send({ email: 'test@example.com', password: 'Password123', deviceFingerprint: 'fp-test' });
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('accessToken');
expect(res.body).toHaveProperty('refreshToken');
expect(res.body.user.email).toBe('test@example.com');
});
});
// tests/e2e/auth/login.spec.ts (Playwright)
import { test, expect } from '@playwright/test';
test.describe('Login Flow E2E', () => {
test('TC-LOGIN-001: user logs in via UI', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid=email]', 'test@example.com');
await page.fill('[data-testid=password]', 'Password123');
await page.click('[data-testid=submit]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid=user-name]')).toContainText('Test User');
});
});
性能测试用例:
| 用例ID | 场景 | 并发 | 目标 RT(P99) | 通过标准 |
|---|---|---|---|---|
| PERF-LOGIN-001 | 正常登录 | 100 VU | < 500ms | 0% 错误率 |
| PERF-LOGIN-002 | 并发刷新令牌 | 500 VU | < 300ms | 0% 错误率 |
安全测试用例:
| 用例ID | 攻击向量 | 预期结果 |
|---|---|---|
| SEC-LOGIN-001 | SQL 注入 email 字段 | 400 错误、无数据泄露 |
| SEC-LOGIN-002 | 暴力破解 | 5 次失败后锁定 5 分钟 |
| SEC-LOGIN-003 | 令牌重放 | refreshToken 单次使用、重放拒绝 |
CI 配置片段:
.github/workflows/test.yml:
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:unit -- --coverage
integration:
needs: unit
runs-on: ubuntu-latest
services:
postgres: ...
redis: ...
steps:
- run: npm run test:integration
e2e:
needs: integration
runs-on: ubuntu-latest
steps:
- uses: microsoft/playwright-github-action@v1
- run: npm run test:e2e