工具链阿新聊ai

Superpowers 工作流 · TDD 是怎么被严格执行的

Superpowers 工作流 · TDD 是怎么被严格执行的 更新日期:2025/06 TL;DR: Superpowers 通过「铁律」强制执行 TDD——写了代码先于测试?删掉重来。RED GREEN REFACTOR 三个阶段每步都验证:测试先写并确实失败、最小实现、重构保持测试通过。违反规则的合理化借口都被...

Superpowers 工作流 · TDD 是怎么被严格执行的

更新日期:2025/06

TL;DR: Superpowers 通过「铁律」强制执行 TDD——写了代码先于测试?删掉重来。RED-GREEN-REFACTOR 三个阶段每步都验证:测试先写并确实失败、最小实现、重构保持测试通过。违反规则的合理化借口都被列在 rationalization 表里,AI 识别到就触发「删掉重启」流程。

为什么要强制执行

软件开发里最容易妥协的是测试。我们都知道 TDD 好,但总有理由:「这个太简单了」、「我先探探路」、「写完再补测一样」。Superpowers 的判断是:一旦开口子,TDD 就消失了。

理由很简单。测试写在实现之后,测试通过不证明任何东西——它可能测错了东西、测了实现而非行为、漏掉了你忘记的边界情况。你从来没见过测试抓住那个 bug,因为实现的时候 bug 就不在了。

Superpowers 的解决方案不是「建议 TDD」,而是「强制执行」。AI 在实现任何功能前,必须先写测试、看它失败、写最小实现、看它通过、重构。写了代码再回头写测试?直接删掉代码,从测试开始重做。没有「先留着当参考」这种选项。

这不是教条,是工程实践。你保留那两小时没测试的代码,留下的是两小时技术债和零置信度。删掉重写要再花两小时,但你得到的是测试覆盖过的可信任代码。

RED-GREEN-REFACTOR 执行细节

Superpowers 把 TDD 拆成三个阶段,每个阶段都有强制验证步骤。跳过任何一步,AI 都会被要求重新执行。

RED - 写失败的测试

第一件事不是写代码,是写测试。测试必须满足三个条件:只测一个行为、名称清晰、用真实代码而非 mock。

// 好的测试
test('重试失败操作 3 次', async () => {
  let attempts = 0;
  const operation = () => {
    attempts++;
    if (attempts < 3) throw new Error('fail');
    return 'success';
  };

  const result = await retryOperation(operation);

  expect(result).toBe('success');
  expect(attempts).toBe(3);
});

这个测试测一个行为(重试 3 次后成功),名称说清楚发生了什么,用的是真实代码而非 mock。

// 不好的测试
test('retry works', async () => {
  const mock = jest.fn()
    .mockRejectedValueOnce(new Error())
    .mockRejectedValueOnce(new Error())
    .mockResolvedValueOnce('success');
  await retryOperation(mock);
  expect(mock).toHaveBeenCalledTimes(3);
});

名称模糊,测的是 mock 行为而非真实代码,看不出 retryOperation 应该怎么用。

验证 RED - 看测试失败

这是最关键的一步。不能跳过

npm test path/to/test.test.ts

AI 必须确认三件事:

  • 测试失败(不是报错)
  • 失败信息是预期的
  • 失败原因是功能缺失(不是拼写错误)

如果测试通过了?说明你在测试已存在行为,不是定义新行为。修正测试。

如果测试报错了?说明测试代码有问题,修好后再跑。

不跑测试直接实现?Superpowers 的 skill 会在执行流程里拦截。test-driven-development skill 会在实现代码前被触发,强制要求写测试并验证失败。

GREEN - 最小实现

写最简单的代码让测试通过。不要添加功能、重构其他代码、「顺便改进」。

// 好的实现 - 刚好够通过
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
  for (let i = 0; i < 3; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === 2) throw e;
    }
  }
  throw new Error('unreachable');
}
// 不好的实现 - 过度设计
async function retryOperation<T>(
  fn: () => Promise<T>,
  options?: {
    maxRetries?: number;
    backoff?: 'linear' | 'exponential';
    onRetry?: (attempt: number) => void;
  }
): Promise<T> {
  // YAGNI - 测试没要求的特性
}

测试只要求重试 3 次,你就只实现重试 3 次。后面的配置选项、回调函数、退避策略,等测试要求再写。

验证 GREEN - 看测试通过

npm test path/to/test.test.ts

AI 必须确认:

  • 测试通过
  • 其他测试依然通过
  • 输出干净(无错误、无警告)

测试失败?修代码,别改测试。

其他测试挂了?现在就修。不要留着「回头再修」。

REFACTOR - 清理代码

只有测试全绿之后才重构:

  • 删除重复代码
  • 改善命名
  • 提取辅助函数

重构时保持测试通过。不要添加新行为。

重复循环

下一个功能,下一个失败的测试。

"未测先写就删掉"的铁律

Superpowers 的核心规则写在 skill 里:

NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST

不是「推荐」,是强制

AI 检测到以下任何情况,都会触发删代码重来的流程:

  • 代码写在测试之前
  • 测试通过后写的(说明测的是已存在行为)
  • 测试没看过失败就写了实现
  • 「我先留着当参考」
  • 「我写完再补测试」

处理方式很简单:rm 删掉代码,从 RED 阶段开始。

为什么要这么极端?因为「先留着」是个谎言。你会在写测试的时候不自觉地参考那堆代码。测试不再是「定义应该做什么」,变成「验证我写的代码做什么」。你失去了 TDD 的核心价值——通过测试先思考行为,再考虑实现。

更糟的是,保留的代码可能有 bug。你为 buggy 的代码写测试,测试可能就在验证那个 bug。删掉重写看起来浪费,但保留问题代码的浪费更大:你要花时间调试、修 bug、补测试,最后还是得回到 TDD。

Testing Anti-Patterns 的核心

Superpowers 有个独立的 reference 文件 testing-anti-patterns.md,列出了测试中最容易犯的错误。AI 在写测试时会被要求参考这份文件,避免掉坑。

测试 Mock 行为而非真实行为

// ❌ 错误:测试 mock 存在性
test('renders sidebar', () => {
  render(<Page />);
  expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});

你在验证 mock 在那,不是 sidebar 真的渲染了。测试在 mock 存在时通过,不在时失败,但和真实行为无关。

// ✅ 正确:测真实组件或不 mock
test('renders sidebar', () => {
  render(<Page />);  // 别 mock sidebar
  expect(screen.getByRole('navigation')).toBeInTheDocument();
});

生产类里的测试专用方法

// ❌ 错误:destroy() 只在测试里用
class Session {
  async destroy() {  // 看起来像生产 API!
    await this._workspaceManager?.destroyWorkspace(this.id);
  }
}

生产类被测试代码污染了。这个方法如果被意外调用在生产里,会删掉用户的 session。

// ✅ 正确:测试工具处理清理
export async function cleanupSession(session: Session) {
  const workspace = session.getWorkspaceInfo();
  if (workspace) {
    await workspaceManager.destroyWorkspace(workspace.id);
  }
}

// 测试里
afterEach(() => cleanupSession(session));

不理解依赖就 Mock

// ❌ 错误:mock 破坏了测试依赖的行为
test('detects duplicate server', () => {
  vi.mock('ToolCatalog', () => ({
    discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
  }));

  await addServer(config);
  await addServer(config);  // 应该抛错——但不会!
});

mock 的方法有测试需要的副作用(写配置),但被你删掉了。测试会通过,但不是因为你测对了。

// ✅ 正确:在正确层级 mock
test('detects duplicate server', () => {
  vi.mock('MCPServerManager'); // 只 mock 慢的服务器启动

  await addServer(config);  // 配置写入
  await addServer(config);  // 重复检测 ✓
});

不完整的 Mock

// ❌ 错误:只 mock 你知道的字段
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: 'Alice' }
  // 缺失:下游代码用的 metadata
};

// 后来:代码访问 response.metadata.requestId 时挂了

部分 mock 隐藏了结构性假设。你只 mock 了你知道的字段,但下游代码可能依赖你没包含的字段。测试通过,集成失败。

// ✅ 正确:镜像真实 API 完整性
const mockResponse = {
  status: 'success',
  data: { userId: '123', name: 'Alice' },
  metadata: { requestId: 'req-789', timestamp: 1234567890 }
  // 真实 API 返回的所有字段
};

Rationalization 表

Superpowers 识别到合理化借口时,会拒绝执行并列出为什么。以下是完整的 rationalization 表:

借口 现实
「太简单不值得测」 简单代码也会坏。写个测试 30 秒。
「我回头再测」 测试立即通过证明不了什么。
「回头测效果一样」 测试后回答「这代码干嘛?」测试前回答「这代码该干嘛?」
「我手动测过所有边界情况」 临时 ≠ 系统。没记录,无法重跑。
「删掉 X 小时的成果是浪费」 沉没成本谬误。保留未验证代码才是技术债。
「留着当参考,先写测试」 你会不自觉适配它。删掉就是删掉。
「我需要先探探路」 可以。扔掉探索代码,TDD 起手。
「难测说明设计不清楚」 听测试的。难测 = 难用。
「TDD 会拖慢我」 TDD 比 debug 快。实用就是测试先行。
「手动测更快」 手动无法证明边界情况。每次改代码都要重测。
「现有代码没测试」 你在改进它。为现有代码加测试。

这个表不是装饰。当 AI 说「这个太简单了」,Superpowers 会回:「简单代码也会坏。写个测试 30 秒。」当 AI 说「我先留着当参考」,Superpowers 会回:「你会不自觉适配它。删掉就是删掉。」

红旗标志

出现以下任何情况,AI 必须删掉代码、从 TDD 重新开始:

  • 代码写在测试之前
  • 实现后才写测试
  • 测试立即通过
  • 无法解释为什么测试失败
  • 测试是「回头」加的
  • 合理化「就这一次」
  • 「我手动测过了」 | 「回头测效果一样」
  • 「是精神不是仪式」
  • 「留着当参考」或「适配现有代码」
  • 「已经花了 X 小时,删掉是浪费」
  • 「TDD 是教条,我这是实用」
  • 「这个情况不同因为...」

所有这些信号触发同一个动作:删代码,TDD 重新来。

验证清单

Superpowers 要求 AI 在标记工作完成前检查每一项:

  • 每个新函数/方法都有测试
  • 每个测试都在实现前看过它失败
  • 每个测试都因预期原因失败(功能缺失,不是拼写错误)
  • 写了最小代码来通过每个测试
  • 所有测试通过
  • 输出干净(无错误、无警告)
  • 测试用真实代码(只在无法避免时用 mock)
  • 边界情况和错误处理覆盖了

无法勾选全部?说明你跳过 TDD。重来。

权衡与局限

Superpowers 的强制 TDD 不是免费午餐。

成本: 严格 TDD 确实慢过「先写后测」。一个 2 小时的功能,TDD 可能要 2.5 小时。差出来的 30 分钟,你在写测试、看测试失败、重构。但你得到的是可信任的代码、自动化回归保护、可读的行为文档。不做 TDD 的 2 小时,后面要花 4 小时 debug。

适用范围: Superpowers 的 TDD 强制适用于新功能、bug 修复、重构、行为变更。以下情况允许例外(但需人工确认):一次性原型、生成的代码、配置文件。例外不是默认,是人工显式批准的。

学习曲线: 严格 TDD 需要练习。一开始会觉得别扭:「为什么一定要看测试失败?」但习惯了之后,你会发现看不失败就写代码反而更别扭——你怎么知道自己写的代码是对还是错?

团队阻力: 团队里有人不信 TDD?Superpowers 的 skill 会在 AI 层面强制执行,但团队文化仍然需要认同。如果有人手动绕过 skill、直接写代码,强制执行就失效了。你需要团队共识。

延伸阅读

评论

0
登录后可以参与评论和讨论。
💬

还没有评论

欢迎留下第一条评论,帮助这篇内容更快形成讨论。