
“先求其可证,再求其可用;先见其失败,方知其修正。”
老李最近在看一段很典型的 AI 编码记录:需求只有三件事,登录、注册、个人中心。Agent 两分钟就吐出一整套 Controller、Service、Mapper、SQL,看起来像是一个完整项目,甚至还顺手补了几条“测试”。
问题也在这里。那几条测试不是为了证明行为,而是为了给已经写好的实现盖章。邮箱重复能不能挡住?密码错了会不会返回 401?没有 token 能不能访问个人中心?这些关键问题,测试里都没真正被逼出来。
这就是 TDD Skill 的价值。它不是“让 AI 多写测试”这么朴素,而是把顺序改掉:先写失败测试,再写最小实现,最后在绿色状态下重构。顺序一变,AI 最容易走的偷懒路就被堵上了:
对架构师来说,TDD 解决的不是“代码写得快不快”,而是“我凭什么相信它真的对”。在 AI 时代,这个问题比速度更要命。
老李见过最常见的场景,就是一个 Spring Boot 新人项目:登录、注册、个人中心,业务不复杂,但如果 AI 先写实现,最后很容易变成下面三种状态:
这些问题在“看代码”时不一定一眼暴露,只有测试先站出来,它们才会自己冒头。
典型症状:
维度 | 没有 Skill | 有 test-driven-development Skill |
|---|---|---|
开发顺序 | 先实现,后补测 | 先写失败测试,再写实现 |
Bug 修复 | 改完就算 | 先复现,再修复,再保留回归用例 |
结果可信度 | 依赖人的直觉 | 依赖机器证明 |
Token 消耗 | 高(来回返工) | 低(路径清晰) |
团队一致性 | 依赖个人习惯 | 统一约束,自动执行 |
TDD Skill 解决的是“行为可信”的问题。它把不确定的口头需求,翻译成能失败、能通过、能回归的测试。它还能把 AI 最喜欢的“顺手多写一点”压回最小实现,让代码先为当前行为服务,再谈优化和抽象。
“无证之言,风一吹就散;有证之行,改一版也不慌。”
name: test-driven-development
source: prpm.dev/search?q=tdd+skill
install: 在 PRPM 中搜索 TDD skill,按平台给出的安装命令安装
触发方式: 新功能、Bug 修复、重构、行为变更
适用 Agent: Claude Code / Codex / Cursor / CodeBuddy / Trae / OpenCode
规则 1:先写失败测试
原文意图:Write a failing test before writing the code that makes it pass.
中文解读:测试如果一开始就绿,不能证明它在验证新行为,只能证明它碰巧经过了旧逻辑。TDD 要求先看到红灯,再动手修。
规则 2:Bug 修复先复现
原文意图:For bug fixes, reproduce the bug with a test before attempting a fix.
中文解读:没有复现测试的修复,只是一次“手工碰运气”。一旦需求再动,Bug 还会回来。
规则 3:GREEN 只写最小实现
原文意图:Write minimal code to pass the test.
中文解读:AI 特别容易顺手把未来可能用到的能力一起写了。TDD 把这种冲动压住,只让它做当前测试要求的事。
规则 4:绿色之后才能重构
原文意图:Clean up the implementation after tests are green.
中文解读:先保证行为,再谈结构。这样重构时不需要靠感觉,测试会替你盯住回归。
“工具只是门,顺序才是道。”
# 把 TDD skill 安装到你的 skills 目录或技能包仓库
npx skills add <owner>/<repo>/test-driven-development
# 如果你是手动管理 skill 仓库,就把 SKILL.md 放进可被 Agent 读取的目录
# 例如 ~/.agents/skills/test-driven-development把 TDD skill 放进 Claude Code 可读取的 skills 目录后,重启会话,让它在你说“先按 TDD 来”时优先走测试流程。验证方式很直接:让它处理一个新接口,观察它是否先产出失败测试,而不是先写 Controller。
在 Codex 的技能目录里安装后,建议用任务式触发词启动:
请按 TDD 完成登录注册接口,先写失败测试,再写最小实现,最后重构。
如果它开始先讲架构、先铺目录、先写实现,那就说明 skill 没真正吃进去。
在 Cursor 中把 TDD 规则绑定到项目规则或技能目录后,优先看它是不是在生成代码前先补测试。Cursor 很容易顺着已有代码继续展开,所以 TDD 最适合拿来卡住“顺手多写一点”的惯性。
把 TDD 规则收进项目级 skill 配置后,重点不是看它会不会写测试,而是看它会不会主动把“先实现后补测”改成“先测后写”。这是 TDD 真正的分水岭。
Trae 里建议把 TDD 当成项目级硬规则使用。触发后只看三件事:有没有失败测试、失败原因对不对、最小实现是不是只够过这一条测试。
OpenCode 场景下同样适用。你不需要它一次就把所有测试都写完,你只需要它把每一次行为变化都变成一个可重复运行的红绿循环。
下面三个提示词,基本能测出这个 Skill 有没有真的生效:
测试 Prompt 1(正向触发):
“给我做一个 Spring Boot 登录注册接口。先写失败测试,再实现代码。”
预期行为:
先给测试用例和失败原因,再给最小实现,不会先铺大段业务代码。
测试 Prompt 2(Bug 复现):
“空邮箱能注册,帮我修复。先写复现测试。”
预期行为:
先产出复现 Bug 的测试,再修代码,最后保留回归用例。
测试 Prompt 3(边界验证):
“我已经写了实现,你直接帮我重构就行。”
预期行为:
Skill 会提醒先确认测试是否存在,必要时要求先补测试再重构。
“纸上谈 TDD,终究是讲道理;跑起来的测试,才叫证据。”
我们用一个最小的 Spring Boot + MySQL 项目演示:登录、注册、个人中心。
任务描述: 为用户服务新增邮箱注册、登录、个人中心接口。
技术栈: Spring Boot 3 + Java 17 + Spring JDBC + MySQL 8 + BCrypt + JUnit 5 + Testcontainers
src/main/java/com/example/usercenter
├── auth
│ ├── AuthController.java
│ ├── AuthService.java
│ └── TokenService.java
├── user
│ ├── UserRepository.java
│ └── UserProfileController.java
└── UserCenterApplication.java
src/test/java/com/example/usercenter
└── auth
└── AuthFlowTest.java@SpringBootTest
@Testcontainers
class AuthFlowTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.36")
.withDatabaseName("tdd")
.withUsername("tdd")
.withPassword("tdd");
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private TestRestTemplate rest;
@Test
void register_login_and_get_profile_should_work() {
var reg = rest.postForEntity("/api/register",
Map.of("email", "a@demo.com", "password", "123456", "nickname", "老李"),
Map.class);
assertThat(reg.getStatusCode()).isEqualTo(HttpStatus.CREATED);
var login = rest.postForEntity("/api/login",
Map.of("email", "a@demo.com", "password", "123456"),
Map.class);
assertThat(login.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(login.getBody()).containsKey("token");
}
}这一步的意义,不是“写个测试看起来专业”,而是逼出接口边界:接口到底叫什么、返回什么、异常怎么处理,都必须先落到测试里。
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
private final TokenService tokenService;
public Long register(String email, String password, String nickname) {
if (userRepository.existsByEmail(email)) {
throw new DuplicateEmailException(email);
}
var user = new UserEntity(null, email, passwordEncoder.encode(password), nickname, "");
return userRepository.insert(user);
}
public String login(String email, String password) {
var user = userRepository.findByEmail(email)
.orElseThrow(() -> new UnauthorizedException("bad credentials"));
if (!passwordEncoder.matches(password, user.passwordHash())) {
throw new UnauthorizedException("bad credentials");
}
return tokenService.issue(user.id());
}
}这段代码不追求优雅,只追求刚好把测试变绿。TDD 在这里反对的不是代码短,而是“把未来可能的需求提前塞进去”。
绿色之后再做三件事:
AuthService 挪到 TokenService。@RestControllerAdvice。curl -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"email":"a@demo.com","password":"123456","nickname":"老李"}'
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"email":"a@demo.com","password":"123456"}'
curl http://localhost:8080/api/me \
-H "Authorization: Bearer <token>"Bug 描述: 重复邮箱居然还能注册成功。
修复方式: 先写复现测试,再修实现。
@Test
void duplicate_email_should_be_rejected() {
rest.postForEntity("/api/register",
Map.of("email", "dup@demo.com", "password", "123456", "nickname", "A"),
Map.class);
var second = rest.postForEntity("/api/register",
Map.of("email", "dup@demo.com", "password", "123456", "nickname", "B"),
Map.class);
assertThat(second.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
}这条测试先红,再把 UserRepository.existsByEmail() 和数据库唯一约束接上,最后才变绿。这样下次改注册逻辑时,它还能继续替你看门。
适用条件: 需求稍大、测试较多、需要分工时。
Orchestrator Agent
├── 负责:拆任务、安排 RED/GREEN/REFACTOR 顺序
├── 使用 Skill:test-driven-development
│
├── Subagent A:写失败测试和复现用例
│ └── 使用 Skill:test-driven-development
│
└── Subagent B:按测试写最小实现并重构
└── 使用 Skill:test-driven-development这个分工里,最重要的不是“谁会写代码”,而是“谁先把问题变成可失败的证据”。TDD 让分工边界清晰得多。
“能跑通,不等于能相信;能复现,才算真修好。”
这个 Skill 的本质是: 把“我觉得对”改成“测试证明对”。
从架构师角度:
TDD 解决的是 AI 协作里的信任问题。Agent 可以写得很快,但速度不是质量。真正让团队敢放手的,是它每次都把行为变成测试,把修复变成证据,把重构变成可回退的过程。对团队来说,TDD Skill 更像一条工程纪律,而不是一个编程偏好。
ROI 估算:
场景 | 引入前 | 引入后 | 节省估算 |
|---|---|---|---|
新接口开发 | 先写实现,返工后补测 | 先红后绿,路径清楚 | 少一次大回改 |
Bug 修复 | 修完就走,回归靠记忆 | 复现测试长期保留 | 少反复踩坑 |
团队 Review | 讨论“像不像对” | 讨论“测试有没有覆盖” | 评审更快 |
把你定制化的 TDD Skill 发布到 GitHub,最实用的不是“我也有个仓库”,而是让团队以后每次做业务都顺着同一套验证顺序走。
mkdir -p skills/test-driven-development
cat > skills/test-driven-development/SKILL.md << 'EOF'
name: test-driven-development
description: 先写失败测试,再写最小实现,再重构
EOF
git add skills/test-driven-development/
git commit -m "feat: add tdd skill"
git push origin main
系列索引模板:
期数 | Skill | 核心价值 | 安装 |
|---|---|---|---|
#011 | test-driven-development | 让测试先于实现 |
|
学完 test-driven-development,你会马上碰到一个新问题:
测试能证明代码行为对了,但页面真的在浏览器里对了吗?
这就是下一期最自然的延伸:webapp-testing。
它解决的是从“代码行为正确”走到“真实浏览器路径正确”的那一步。
问题场景: 登录接口单测全绿,但页面上按钮还是失效、样式还是错位、真实跳转还是报错。
下一期预告: webapp-testing — 把“能跑”的感觉,变成“真能用”的证据。
环节 | 关键动作 | 工具/命令 | 注意事项 |
|---|---|---|---|
安装 | 把 skill 放进可读取目录 |
| 重启会话后生效 |
验证 | 先触发一个新功能任务 | 对话框 / Goal | 先看有没有失败测试 |
新接口开发 | RED → GREEN → REFACTOR |
| 不要先写实现 |
Bug 修复 | 先复现,再修复 |
| 复现测试要长期保留 |
发布 | GitHub + README 索引 |
| 保持技能可复用 |
TDD 的本质不是更会写代码,而是更敢相信代码。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。