名称: tdd-strict 描述: 强制执行严格的测试驱动开发,遵循红-绿-重构循环。在没有先前测试的情况下阻止代码生成。文档化13种无效的理性化。在新功能、错误修复、重构时激活。
严格测试驱动开发
此技能基于核心原则强制执行TDD实践:
“如果你没有看到测试失败,你就不知道它是否测试了正确的东西。”
何时激活
- 在每次新功能实现时
- 在错误修复时(首先测试重现错误,然后修复)
- 在重构时(测试必须在更改前后通过)
- 在API扩展时
- 在每个导出的函数时
红-绿-重构循环
1. 红: 编写失败的测试
// 首先: 编写测试
describe('calculateOEE', () => {
it('当可用性为0时应返回0', () => {
const result = calculateOEE({ availability: 0, performance: 100, quality: 100 });
expect(result).toBe(0);
});
});
// 测试必须失败:
// 错误: calculateOEE 未定义
// 或
// 错误: 期望0但收到未定义
重要: 测试必须因正确原因失败:
- 函数不存在
- 函数返回错误结果
- 非: 测试本身的语法错误
2. 绿: 使测试通过的最小代码
// 之后: 最小代码
export function calculateOEE(params: OEEParams): number {
if (params.availability === 0) return 0;
// 后续逻辑通过更多测试添加
return 0;
}
规则: 编写使测试通过的最简单代码。
- 无优化
- 无额外功能
- 无“明显”扩展
3. 重构: 清理而不添加新行为
// 经过多个绿色测试后: 允许重构
export function calculateOEE({ availability, performance, quality }: OEEParams): number {
return (availability * performance * quality) / 10000;
}
重构规则:
- 所有现有测试必须保持通过
- 不添加新行为
- 仅改进代码结构
- 每一步重构后: 运行测试
13种无效的理性化
这些借口永远不可接受:
1. “太简单了不需要测试”
现实: 简单代码需要简单测试。1行测试也可以。
it('应添加两个数字', () => {
expect(add(2, 3)).toBe(5);
});
2. “我稍后测试”
现实: “稍后”意味着“永不”。TDD意味着测试在先。
3. “已经手动测试了”
现实: 手动测试不可重现且不具扩展性。
4. “时间压力不允许测试”
现实: 测试在调试时节省时间并防止回归错误。
5. “私有方法不需要测试”
现实: 通过公共API测试行为。如果不可测试: 重构。
6. “UI代码不能测试”
现实: React Testing Library, Playwright, Storybook 正是为此存在。
7. “逻辑很琐碎”
现实: 琐碎逻辑会变化。测试记录预期行为。
8. “我们有QA流程”
现实: QA后期发现错误且成本更高。TDD从一开始防止错误。
9. “代码只是临时的”
现实: 临时代码常存活数年。测试也保障临时代码。
10. “测试减慢开发速度”
现实: TDD长期通过减少调试和回归错误加速开发。
11. “遗留代码没有测试”
现实: 在更改前编写特性化测试。逐步改进。
12. “框架/库已经测试了”
现实: 测试你对框架的使用,而非框架本身。
13. “模拟太繁琐”
现实: 如果模拟太复杂,设计就太复杂。重构。
验证清单
每次提交前必须满足:
- [ ] 每个导出函数至少有一个测试
- [ ] 每个测试在实现前失败(观察红)
- [ ] 每个测试只检查一件事(单一断言原则)
- [ ] 每个测试的最小代码(无过度工程)
- [ ] 边缘情况覆盖:
- [ ] 空/未定义输入
- [ ] 空数组/字符串
- [ ] 边界值(0, 最大整数, 负数)
- [ ] 错误输入(预期类型错误)
- [ ] 测试名称描述行为:
当[条件]时应[预期结果] - [ ] 提交中无
skip或only测试 - [ ] 新代码覆盖率至少80%
实践中的TDD工作流
步骤1: 创建测试文件
# 对于src/utils/oee.ts中的新函数
touch src/utils/oee.test.ts
步骤2: 最小失败测试
// src/utils/oee.test.ts
import { describe, it, expect } from 'vitest';
import { calculateOEE } from './oee';
describe('calculateOEE', () => {
it('对于样本制造数据应返回85', () => {
const result = calculateOEE({
availability: 90,
performance: 95,
quality: 99
});
expect(result).toBeCloseTo(84.645, 2);
});
});
步骤3: 运行测试(必须失败)
npm run test -- --run src/utils/oee.test.ts
# 预期输出:
# 错误: 无法找到模块 './oee'
# 或存根后:
# 错误: 期望84.645但收到未定义
步骤4: 最小实现
// src/utils/oee.ts
export interface OEEParams {
availability: number;
performance: number;
quality: number;
}
export function calculateOEE(params: OEEParams): number {
return (params.availability * params.performance * params.quality) / 10000;
}
步骤5: 运行测试(必须通过)
npm run test -- --run src/utils/oee.test.ts
# 预期输出:
# 通过 src/utils/oee.test.ts
步骤6: 下一个边缘情况测试
it('当值超过100时应抛出错误', () => {
expect(() => calculateOEE({
availability: 101,
performance: 100,
quality: 100
})).toThrow('值必须在0到100之间');
});
回到步骤3(红) -> 步骤4(绿) -> 重复。
识别反模式
禁止: 代码后测试
// 错误: 先写代码
function add(a: number, b: number): number {
return a + b;
}
// 然后才写测试 - 无效!
// 你不知道测试是否正确
禁止: 一次写太多代码
// 错误: 无测试实现完整类
class UserService {
async create(user: User) { ... }
async update(id: string, data: Partial<User>) { ... }
async delete(id: string) { ... }
async findById(id: string) { ... }
async findAll(filters: FilterOptions) { ... }
}
// 正确: 用TDD逐个方法实现
禁止: 调整测试以通过
// 错误: 因实现不同而更改测试
// 之前: expect(result).toBe(100);
// 之后: expect(result).toBe(99.5); // “因为公式这样输出”
// 正确: 修正实现或澄清需求
框架特定模式
React组件 (Vitest + Testing Library)
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('LoginButton', () => {
it('点击时应显示加载旋转器', async () => {
render(<LoginButton onLogin={vi.fn()} />);
await userEvent.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
});
API端点 (Supertest)
import request from 'supertest';
import { app } from '../app';
describe('POST /api/analyze', () => {
it('无认证时应返回401', async () => {
const response = await request(app)
.post('/api/analyze')
.send({ data: 'test' });
expect(response.status).toBe(401);
expect(response.body.error).toBe('未授权');
});
});
异步代码
describe('fetchUserData', () => {
it('网络失败时应重试3次', async () => {
const mockFetch = vi.fn()
.mockRejectedValueOnce(new Error('网络'))
.mockRejectedValueOnce(new Error('网络'))
.mockResolvedValueOnce({ id: 1, name: '测试' });
const result = await fetchUserData(1, { fetch: mockFetch });
expect(mockFetch).toHaveBeenCalledTimes(3);
expect(result.name).toBe('测试');
});
});
违反TDD时
- 停止: 无测试不生成更多代码
- 警告: “检测到TDD违规: [描述]”
- 指导: 展示正确的TDD工作流
- 询问: “我应该先写测试吗?”
成功度量
- 测试与代码比率: 至少1:1(测试行数与代码行数)
- 覆盖率: 新代码至少80%
- 红绿时间: 短周期(每功能5-10分钟)
- 测试执行时间: 单元测试少于10秒
命令
# 运行单个测试(检查红)
npm run test -- --run path/to/file.test.ts
# 所有测试(提交前)
npm run test
# 覆盖率报告
npm run test:coverage
# 监视模式(开发中)
npm run test -- --watch
与其他技能集成
- code-quality-gate: TDD测试是门1(预提交)的一部分
- strict-typescript-mode: 测试也必须类型安全
- supervisor: 检查代理验证TDD合规性
来源和进一步阅读
- Kent Beck: “测试驱动开发: 通过示例”
- Robert C. Martin: “清洁代码”(第9章: 单元测试)
- Martin Fowler: “重构”(重构时的测试安全)