严格测试驱动开发Skill tdd-strict

这个技能用于强制执行严格的测试驱动开发(TDD)实践,确保在编写代码之前先编写测试,遵循红-绿-重构循环,并避免无效的理性化。它适用于新功能、错误修复和重构,提供验证清单和最佳实践,以提高代码质量和开发效率。关键词:测试驱动开发,TDD,单元测试,红绿重构,代码测试,软件开发,自动化测试,质量保障。

测试 0 次安装 0 次浏览 更新于 3/18/2026

名称: 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, 最大整数, 负数)
    • [ ] 错误输入(预期类型错误)
  • [ ] 测试名称描述行为: 当[条件]时应[预期结果]
  • [ ] 提交中无skiponly测试
  • [ ] 新代码覆盖率至少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时

  1. 停止: 无测试不生成更多代码
  2. 警告: “检测到TDD违规: [描述]”
  3. 指导: 展示正确的TDD工作流
  4. 询问: “我应该先写测试吗?”

成功度量

  • 测试与代码比率: 至少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: “重构”(重构时的测试安全)