SuperAPI AI 工具集

tdd

测试与质量

查看项目

更新于 10-15 15:32

技能介绍

用途

TDD 是一个测试驱动开发技能,用于在构建功能或修复缺陷时采用红-绿-重构循环。当用户希望以 TDD 方式开发、提到“red-green-refactor”或需要测试先行的工作流时使用。

核心理念

核心原则:测试应通过公共接口验证行为,而非实现细节。代码可以完全改变,测试不应随之改变。

好的测试是集成风格的:它们通过公共 API 执行真实代码路径,描述系统做什么,而不是怎么做。好的测试读起来像规格说明——“用户可以用有效购物车结账”明确告诉你存在什么能力。这些测试能在重构后存活,因为它们不关心内部结构。

好测试的特征:

  • 测试用户/调用者关心的行为
  • 仅使用公共 API
  • 在内部重构后依然有效
  • 描述做什么,而非怎么做
  • 每个测试一个逻辑断言

坏测试与实现耦合:模拟内部协作者、测试私有方法,或通过外部手段验证(如直接查询数据库而非使用接口)。警告信号:重构时测试失败,但行为并未改变。

危险信号:模拟内部协作者、测试私有方法、断言调用次数/顺序、重构后无行为变化却测试失败、测试名称描述怎么做而非做什么。

优先在实现之前编写测试。如果已经写了代码,考虑从测试重新开始,而不是事后补测试——事后写的测试倾向于验证你构建了什么,而非需求是什么。

模拟策略

仅在系统边界模拟:外部 API(支付、邮件等)、数据库(有时——优先使用测试数据库)、时间/随机性、文件系统(有时)。

不要模拟:自己的类/模块、内部协作者、任何你控制的东西。

使用依赖注入——传入外部依赖,而不是在内部创建它们。

优先使用 SDK 风格接口——为每个外部操作提供具体函数,使每个函数可独立模拟。

可测试性接口设计

  • 接受依赖,不要创建依赖
  • 返回结果,不要产生副作用
  • 小表面积——更少方法 = 更少测试,更少参数 = 更简单测试设置
  • 深模块(来自《软件设计哲学》):小接口 + 大量实现。设计时问:能减少方法吗?简化参数吗?在内部隐藏更多复杂性吗?

反模式:横向切片

不要先写所有测试,再写所有实现。这是“横向切片”——把 RED 当作“写所有测试”,把 GREEN 当作“写所有代码”。

这会产生糟糕的测试:批量编写的测试测试的是想象的行为,而非实际行为;最终测试的是事物的形状(数据结构、函数签名)而非面向用户的行为;测试对真实变化不敏感——行为破坏时通过,行为正常时失败;在理解实现之前就承诺测试结构。

正确方法:通过追踪弹进行垂直切片。一个测试 → 一个实现 → 重复。每个测试响应上一轮循环中学到的内容。

错误(横向):RED: test1, test2, test3, test4, test5;GREEN: impl1, impl2, impl3, impl4, impl5

正确(垂直):RED→GREEN: test1→impl1;RED→GREEN: test2→impl2;RED→GREEN: test3→impl3……

工作流

1. 规划

在写任何代码之前:与用户确认需要哪些接口变更;确认要测试哪些行为(按优先级);识别深模块机会(小接口、深实现);为可测试性设计接口;列出要测试的行为(不是实现步骤);获得用户对计划的批准。

问:“公共接口应该是什么样?哪些行为最重要需要测试?”

你无法测试一切。与用户确认哪些行为最重要。将测试精力集中在关键路径和复杂逻辑上,而非每个可能的边缘情况。

2. 追踪弹

编写一个确认系统一件事的测试:

RED: 写测试 → 运行测试 → 确认它正确失败 GREEN: 写最小代码 → 运行测试 → 确认它通过

测试立即通过?你在测试现有行为。修正测试。

测试报错(不是断言失败)?先修复错误——报错不等于失败。

这是你的追踪弹——证明路径端到端工作。

3. 增量循环

对每个剩余行为:

RED: 写下一个测试 → 运行测试 → 确认它正确失败 GREEN: 写最小代码 → 运行测试 → 确认它通过

规则:一次一个测试;只写足够通过当前测试的代码;不要预判未来测试;保持测试聚焦于可观察行为。

4. 重构

所有测试通过后,寻找重构候选:提取重复;深化模块(将复杂性移到简单接口后面);在自然处应用 SOLID 原则;考虑新代码揭示了现有代码的什么;每个重构步骤后运行测试。

重构候选:重复 → 提取函数/类;长方法 → 拆分为私有辅助函数;浅模块 → 合并或深化;特性依恋 → 将逻辑移到数据所在处;基本类型偏执 → 引入值对象。

永远不要在 RED 时重构。先到 GREEN。

每轮循环检查清单

  • 测试描述行为,而非实现
  • 测试仅使用公共接口
  • 测试能在内部重构后存活
  • 代码对当前测试是最小的
  • 没有添加推测性功能
  • 写代码前观察测试失败
  • 失败是预期原因(缺少功能,而非拼写错误)
  • 所有其他测试仍然通过

缺陷修复示例

TDD 适用于缺陷修复——先写一个重现缺陷的测试。

缺陷:空邮箱通过验证

RED: 测试“拒绝空邮箱”,运行测试 → 失败(空字符串通过验证)✓ GREEN: 添加检查:if (!email || !email.includes("@")) return { valid: false },运行测试 → 通过 ✓,验证所有其他验证测试仍然通过。

类型测试

编译期类型断言。无运行时——仅 bun typecheck。捕获运行时测试看不到的泛型、条件类型和类型约束回归。

适用:泛型 API、工具类型、复杂推断、映射/条件类型、确保无效用法报错。不适用:字符串属性接受字符串这类琐碎内容。

工具:搜索导出 Expect 和 Equal 的文件。如果没有,创建一个:

export function Expect<T extends true>() {} export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends < T

() => T extends Y ? 1 : 2 ? true : false; export type Not<T extends boolean> = T extends true ? false : true; export type IsAny<T> = 0 extends 1 & T ? true : false; export type IsNever<T> = [T] extends [never] ? true : false;

Expect<T extends true> — 编译错误 = 测试失败 Equal<X, Y> — 精确类型相等(击败 any 拓宽) Not, IsAny, IsNever — 边缘情况守卫(any/never 破坏朴素比较)

正向断言:用块作用域避免名称冲突。

负向测试:@ts-expect-error 必须紧邻错误前一行。始终包含原因。未使用的指令 = 失败测试(约束缺失)。

提示:declare const 用于无运行时的模拟值;type _name = Expect<...> 用于仅类型级断言;/* biome-ignore-all lint */ 在类型专用文件顶部抑制未使用变量警告。

用 bun typecheck 运行。如果编译通过,则测试通过。

适用场景

适用于希望以 TDD 方式构建功能或修复缺陷的开发者,尤其是需要测试先行、红-绿-重构循环、可测试接口设计和类型测试的场景。

限制

资料未提供关于收费、价格、平台支持范围、具体效果或使用步骤的更多信息。