name: tdd description: "测试驱动开发。用户希望测试先行地实现功能或修复缺陷,提到“红—绿—重构”,或希望使用集成测试时使用。"
先让检查失败,再实现一个行为
测试驱动开发把一个行为的预期写成检查,看到它因该行为尚未实现而失败,再编写足够的实现使它通过。
还不清楚 Skill、Agent、安装和调用?先读 从零开始的6节入门课。
先把必要的概念讲清楚
这一课讲怎样让测试成为 AI 开发时的反馈,而不是写完功能后补的一份证明。先理解测试怎样观察行为,再理解红灯和绿灯为何必须亲眼看到。作者把重构放在评审阶段,这是这份 skill 的具体安排。
下面是老师补充的入门说明;原作者的要求保留在中英对照正文中。所有例子均为帮助理解而构造的教学情境。
TDD|测试驱动开发
先用一条测试明确一个小行为,并确认当前实现尚未满足它;再写使其通过的实现,继续下一个行为。“红”是测试确实失败,“绿”是测试通过。关键是测试能分辨对错并反馈,而非仅把写测试的时间提前。
seam|测试接入点
作者在测试语境中指测试进入系统、触发行为并观察结果的公共边界。例如调用“收藏课程”接口,再通过“我的收藏”接口检查结果。入口可以是模块公开函数、服务接口或页面,并非一定是浏览器。选择高层稳定入口能覆盖内部多个步骤;入口少不等于测试场景少。
interface / API|接口
使用者与一项能力交互时遵循的约定,包括可调用什么、传入什么、得到什么、失败怎样表示。接口可以是程序函数,也可以是网络请求。创建收藏接口接收用户和课程信息、返回收藏结果;它不需要向调用者暴露数据库表结构。这里的接口通常不是指页面外观。 本教材涉及更广义的“接口”时,还包括错误、调用顺序和约束等使用约定;不能把接口一概等同于网络 API。
mock|替代真实依赖的测试对象
测试时用可控制的对象替代真实服务,例如让模型调用固定返回一句答案。这样可以稳定检查程序如何处理响应,但无法据此证明真实模型总能生成好答案。过度替代内部组件还会让测试和内部写法绑定,一重构就要改测试。
fixture / harness|测试样本与运行装置
fixture 是为复现或测试准备的已知输入和初始数据,例如固定课程清单。harness 是把代码、输入和检查串起来的运行装置,例如一次命令启动最小服务并断言结果。它们帮助每次在相同条件下比较,而不是凭上次页面看起来怎样判断。
refactoring|重构
在保持约定外部行为的前提下改善内部结构,例如合并重复逻辑、调整职责归属。用户仍能完成同样操作,但代码更容易理解和修改。重构不等于顺便加新需求;测试帮助证明外部行为未被意外改变。
domain glossary|业务领域术语表
规定项目的重要概念叫什么、指什么。domain 在这里是业务领域,不是互联网域名。课程是一组课时,课时是一份学习内容;混用会把“收藏课程”实现成“收藏某一课时”。统一语言不仅统一拼写,还统一概念边界。
ADR|架构决策记录
Architecture Decision Record 的缩写,记录一个重要技术决定、当时为什么选择它,以及接受了什么代价。例如规定所有写入经过后端权限检查。遵循相关 ADR,是在适用范围内延续已确认决定;新需求与之冲突时,应说明冲突和修改理由,而不是默默绕过。
读原文,理解每一步为什么这样做
左右内容按小节对应;窄屏先中文、后英文。两种语言均完整展示,对应讲解紧接在小节之后。译文传达原文要求;老师讲解补充概念、原因、例子与适用边界。
name: tdd description: Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
测试驱动开发
TDD 在这里指“先看到测试失败,再让它通过”的循环。本技能说明怎样让这个循环产生值得长期保留的测试:什么是好测试、从哪里测试、需要避免哪些做法,以及每轮如何执行。每一轮开始前和进行中,都应参考这些规则,不要等代码写完才回头检查。
探索代码库时,如果存在 CONTEXT.md,先阅读它,使测试名称和接口用词与项目业务语言一致;同时遵循涉及区域的架构决策记录。
Test-Driven Development
TDD is the red → green loop. This skill is the reference that makes that loop produce tests worth keeping: what a good test is, where tests go, the anti-patterns, and the rules of the loop. Every section applies on every cycle: consult them before and during the loop, not after.
When exploring the codebase, read CONTEXT.md (if it exists) so test names and interface vocabulary match the project's domain language, and respect ADRs in the area you're touching.
什么是好的测试
测试应通过公开接口验证行为,而不是直接依赖内部实现。只要约定的行为不变,内部代码即使彻底重写,测试也不应因此需要重写。
好测试读起来像一条规格。例如“用户可以使用有效购物车结账”,直接说明系统提供哪种能力。由于它不关心内部如何拆函数或组织文件,所以能够在重构后继续使用。
具体例子见 tests.md,关于模拟依赖的指导见 mocking.md。
What a good test is
Tests verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't. A good test reads like a specification: "user can checkout with valid cart" tells you exactly what capability exists, and it survives refactors because it doesn't care about internal structure.
See tests.md for examples and mocking.md for mocking guidelines.
好测试描述能力,而不盯着内部写法
把测试看成可执行的例子:给系统一个输入,执行动作,检查可观察的结果。测试不是一段“代码应该没问题”的说明,也不是让同一个 AI 再说一遍自己写得对。
例子: 收藏课程 C 后,查询当前用户收藏应包含 C。内部可以先查重再保存,也可以依靠存储约束去重;只要约定结果相同,测试无需因写法改变而重写。若测试要求先调用某私有函数两次,它就把内部实现当成了需求。
“代码可以完全改变,测试不应改变”有前提:公开契约保持不变。用户真正改变了功能规则,测试当然要随规格更新。这里强调抵抗内部重构,不是永久冻结所有断言。
测试接入点:从哪里验证
这里的 seam 指测试使用的公开入口:通过它观察行为,而不伸入模块内部。测试应安排在这些接入点,而不是直接针对内部细节。
只在事先约定的接入点上测试。 写任何测试之前,先列出准备测试的入口,与用户确认;未确认的入口不要直接开始写测试。测试不可能穷尽一切,提前选择入口,是为了把精力投入关键路径和复杂逻辑,而不是无差别覆盖每种细枝末节。
可以问:“这个模块公开的使用接口是什么?我们应该从哪些入口验证?”
如果接口形状本身还没有确定,例如模块应该承担多深的工作、衔接位置在哪里、哪些内容应对外暴露,就调用 Skill 工具加载 codebase-design。它统一解释模块、接口、深度、衔接位置、适配器、调用收益和修改集中性等词汇。它是供查阅的设计参考,不是必须另开一场会话的流程。
Seams: where tests go
A seam is the public boundary you test at: the interface where you observe behavior without reaching inside. Tests live at seams, never against internals.
Test only at pre-agreed seams. Before writing any test, write down the seams under test and confirm them with the user. No test is written at an unconfirmed seam. You can't test everything, so agreeing the seams up front is how testing effort lands on the critical paths and complex logic instead of every edge case.
Ask: "What's the public interface, and which seams should we test?"
When the shape of that interface is itself in question (how deep the module is, where the seam belongs, what the interface should expose), call the Skill tool with "codebase-design" for the vocabulary. It is the shared source of the module, interface, depth, seam, adapter, leverage and locality terms, and it is a reference to consult, not a session to run.
先约定入口,让测试投入覆盖重要行为
测试接入点决定你能看见多大范围的行为。例如只调用格式化标题函数,无法看见收藏保存;调用收藏服务能看见业务流程,却未必看见按钮是否可点击。先列出入口,才能讨论是否真正覆盖用户关心的结果。
作者要求先与用户确认入口,这是其协作流程的硬要求。它想防止 AI 沿着所有内部函数无限补测试,或在最容易写测试的地方制造充分验证的错觉。实际使用要结合已授权范围与项目约定,不要把普通测试写成没有结束的审批链。
你可以怎样回答老师: “保存和去重从收藏接口检验,按钮状态从页面检验;本轮不单独绑定私有去重函数。”这比“多写点测试”精确,也无需你先会写全部测试代码。
需要避免的做法
-
测试与内部实现绑在一起。 例如模拟内部协作者、直接测试私有方法,或者绕过约定接口,从旁路查询数据库来验证。一个明显迹象是:业务行为没有变,只是内部重构,测试却坏了。
-
同义反复式测试(tautological)。 测试断言用与实现相同的方式重新计算预期结果。原文列举的例子包括
expect(add(a, b)).toBe(a + b)、按相同方法手工推导出的快照,以及断言一个常量等于它自身。作者认为,这类检查会“由于构造方式而必然通过,永远不会与代码给出的结果不一致”。作者据此要求:预期结果必须来自独立的正确性依据,例如已经确认正确的具体值、完整推演过的算例,或者规格说明。译者说明: 上面保留了原文的判断,但“必然通过”并不适用于它列出的全部例子。具体原因放在紧随本节的老师讲解中,以免把作者的绝对表述当成无条件成立的结论。
-
把测试和实现横向分成两大批。 不要先写完所有测试,再集中写完所有实现。这样容易依据想象固定测试结构,验证的是设想中的代码形状,而不是用户行为。应改为纵向推进:一个测试、对应的最小实现,再进入下一轮。每条测试像一发探路的“曳光弹”,根据上一轮获得的认识调整下一步。
Anti-patterns
- Implementation-coupled: mocks internal collaborators, tests private methods, or verifies through a side channel (querying the database instead of using the interface). The tell: the test breaks when you refactor but behavior hasn't changed.
- Tautological: the assertion recomputes the expected value the way the code does (
expect(add(a, b)).toBe(a + b), a snapshot derived by hand the same way, a constant asserted equal to itself), so it passes by construction and can never disagree with the code. Expected values must come from an independent source of truth: a known-good literal, a worked example, the spec. - Horizontal slicing: writing all tests first, then all implementation. Bulk tests verify imagined behavior: you test the shape of things rather than user-facing behavior, the tests go insensitive to real changes, and you commit to test structure before understanding the implementation. Work in vertical slices instead: one test → one implementation → repeat, each test a tracer bullet that responds to what the last cycle taught you.
为什么某些看起来很多的测试,几乎不提供证据
第一种是实现耦合:模拟内部每个协作者,再断言它们怎样互相调用。系统外部行为未变,内部重排就全红,维护成本很高。
第二种是预期值缺少独立依据。比如折扣实现错误地使用了某个公式,测试又复制同一公式作为正确答案,两边一起错还会通过。更好的例子是从规则给出“100 元打九折应为 90 元”,让期待结果来自业务规则。原文把某些表达称为“必然通过”很绝对;严格说 expect(add(a,b)).toBe(a+b) 仍可能发现错误实现,问题在于预期依据是否独立、是否可能共享同一误解。
第三种是先批量写完所有测试,再批量实现。你在第一条真实路径尚未打通时,就把后续结构固定了。纵向小步允许第一轮暴露的登录、数据或接口问题,及时改变第二轮的安排。
每轮执行规则
- 先失败,再通过。 先写会失败的测试,再只写足够让它通过的代码。不要提前满足尚未出现的测试,也不要增加设想中的功能。
- 一次只完成一小片行为。 每轮围绕一个接入点、一个测试和一份最小实现推进。
- 本技能把重构安排在审查阶段。 参见
code-review;按作者此处的分工,重构不放在“失败 → 通过”的实现循环里。
Rules of the loop
- Red before green. Write the failing test first, then only enough code to pass it. Don't anticipate future tests or add speculative features.
- One slice at a time. One seam, one test, one minimal implementation per cycle.
- Refactoring is not part of the loop. It belongs to the review stage (see the
code-reviewskill), not the red → green implementation cycle.
一次红灯、一次绿灯,到底应观察什么
测试先行的关键不是文件先后顺序,而是先得到能识别错误的检查。 你需要看到检查在功能尚未实现时失败,随后因实现正确而通过。
假设第一条行为是:“收藏一次后,可以查询到这门课程。”先从约定的收藏接口发起操作,再检查结果。功能未实现时,这条测试应失败。AI 随后只实现支持这一行为所需的代码,并运行到通过。
第二轮才处理:“同一门课程重复收藏,不产生重复记录。”这次先加第二条检查,看到当前实现暴露重复问题,再修正它。每轮都根据已经运行的系统获得信息,而不是一开始就想象十几个内部函数的样子。
为什么结果要有独立依据? 你期待“一条收藏记录”,依据来自产品约定。如果测试复制生产代码的错误逻辑,再用复制出来的结果当答案,两边可能犯同一个错。
不过,原文示例 expect(add(a,b)).toBe(a+b) 并不一定永远通过:如果 add 错误地做减法,这个测试就会失败。真正应警惕的是预期结果没有独立依据,不能把原文所有例子概括成数学上必然自证。
作者在这份技能中把重构放到审查阶段,是自己的流程安排。通常介绍 TDD 时也会讲“失败、通过、重构”。学习时要区分本技能如何分工与 TDD 的常见定义,不要据此认为 TDD 禁止重构。
还有一个实际检查点:第一次失败必须与目标行为有关。如果只是测试文件写错了语法,它并没有证明“重复收藏”这个错误已经被测试捕捉。因此要查看失败原因,不能只看控制台有没有红色文字。
配套参考资料(英文)
先作答,再看参考思路
用自己的话说明:它解决什么问题,完成后会留下什么?
请各用一句话回答。若它只做规划或解释,不要把“已开发”“已部署”写成产物。
为什么把实现中的 reduce 复制进测试算 expected 可能无效?
我已思考,查看参考思路
两处可能共享同一个错误假设。应使用手算已知结果、明确规格或独立参考作为预期,同时从真实入口检查行为。
原文中哪条要求在你的环境下可能不成立?
说出具体一句及其前提,例如工具不可用、资料缺失、已有项目约定冲突,或它只是作者偏好。把你的答案带回课堂,我们据此继续讨论。
把方法放进一个具体情境
教学案例:要求“缺少来源的研究结论不能标记已核验”。先构造无来源输入,断言系统拒绝核验;再实现规则。若只检查某函数被调用一次,仍不能证明用户看到的状态正确。
边界与容易误读的地方
红灯也可能来自环境错误;应确认失败命中目标行为。测试通过不等于需求口径正确,示例答案必须独立确认。
讨论后再实践:先判断上述情境是否适用,再选择真实任务。现在无需安装、运行命令或修改现有项目。