首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >每天一个Skill系列2:test-driven-development 让 AI 先撞上失败测试,再把代码逼到绿色

每天一个Skill系列2:test-driven-development 让 AI 先撞上失败测试,再把代码逼到绿色

原创
作者头像
李福春
发布2026-06-25 11:52:31
发布2026-06-25 11:52:31
3171
举报

一、背景:为什么这个 Skill 很重要

“先求其可证,再求其可用;先见其失败,方知其修正。”

老李最近在看一段很典型的 AI 编码记录:需求只有三件事,登录、注册、个人中心。Agent 两分钟就吐出一整套 Controller、Service、Mapper、SQL,看起来像是一个完整项目,甚至还顺手补了几条“测试”。

问题也在这里。那几条测试不是为了证明行为,而是为了给已经写好的实现盖章。邮箱重复能不能挡住?密码错了会不会返回 401?没有 token 能不能访问个人中心?这些关键问题,测试里都没真正被逼出来。

这就是 TDD Skill 的价值。它不是“让 AI 多写测试”这么朴素,而是把顺序改掉:先写失败测试,再写最小实现,最后在绿色状态下重构。顺序一变,AI 最容易走的偷懒路就被堵上了:

  • 没有测试就先写实现。
  • 实现写完再补一个顺着代码走的测试。
  • 修 Bug 时只改代码,不留复现用例。

对架构师来说,TDD 解决的不是“代码写得快不快”,而是“我凭什么相信它真的对”。在 AI 时代,这个问题比速度更要命。

1.1 问题现场

老李见过最常见的场景,就是一个 Spring Boot 新人项目:登录、注册、个人中心,业务不复杂,但如果 AI 先写实现,最后很容易变成下面三种状态:

  1. 接口能通,但重复邮箱仍然能注册。
  2. 登录返回了 token,但密码错误时也没挡住。
  3. 个人中心能查,但缺少 token 时居然还能看见数据。

这些问题在“看代码”时不一定一眼暴露,只有测试先站出来,它们才会自己冒头。

典型症状:

  • AI 先写业务实现,再倒着补测试,测试一绿就当完成。
  • Bug 修复只改一处逻辑,下次回归又会被同样的问题绊住。
  • 团队每个人都在用 AI,但输出风格和校验标准完全不一致。

1.2 有 vs 没有 Skill 的差别

维度

没有 Skill

有 test-driven-development Skill

开发顺序

先实现,后补测

先写失败测试,再写实现

Bug 修复

改完就算

先复现,再修复,再保留回归用例

结果可信度

依赖人的直觉

依赖机器证明

Token 消耗

高(来回返工)

低(路径清晰)

团队一致性

依赖个人习惯

统一约束,自动执行

1.3 这个 Skill 能解决的核心问题

TDD Skill 解决的是“行为可信”的问题。它把不确定的口头需求,翻译成能失败、能通过、能回归的测试。它还能把 AI 最喜欢的“顺手多写一点”压回最小实现,让代码先为当前行为服务,再谈优化和抽象。


二、Skill 核心约束:中文解读

“无证之言,风一吹就散;有证之行,改一版也不慌。”

2.1 Skill 基本信息

代码语言:yaml
复制
name: test-driven-development
source: prpm.dev/search?q=tdd+skill
install: 在 PRPM 中搜索 TDD skill,按平台给出的安装命令安装
触发方式: 新功能、Bug 修复、重构、行为变更
适用 Agent: Claude Code / Codex / Cursor / CodeBuddy / Trae / OpenCode

2.2 核心约束思维导图

2.3 规则逐条解读

规则 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.

中文解读:先保证行为,再谈结构。这样重构时不需要靠感觉,测试会替你盯住回归。


三、安装指南:各主流 AI IDE

“工具只是门,顺序才是道。”

3.1 通用安装命令

代码语言:bash
复制
# 把 TDD skill 安装到你的 skills 目录或技能包仓库
npx skills add <owner>/<repo>/test-driven-development

# 如果你是手动管理 skill 仓库,就把 SKILL.md 放进可被 Agent 读取的目录
# 例如 ~/.agents/skills/test-driven-development

3.2 Claude Code

把 TDD skill 放进 Claude Code 可读取的 skills 目录后,重启会话,让它在你说“先按 TDD 来”时优先走测试流程。验证方式很直接:让它处理一个新接口,观察它是否先产出失败测试,而不是先写 Controller。

3.3 Codex

在 Codex 的技能目录里安装后,建议用任务式触发词启动:

请按 TDD 完成登录注册接口,先写失败测试,再写最小实现,最后重构。

如果它开始先讲架构、先铺目录、先写实现,那就说明 skill 没真正吃进去。

3.4 Cursor

在 Cursor 中把 TDD 规则绑定到项目规则或技能目录后,优先看它是不是在生成代码前先补测试。Cursor 很容易顺着已有代码继续展开,所以 TDD 最适合拿来卡住“顺手多写一点”的惯性。

3.5 CodeBuddy

把 TDD 规则收进项目级 skill 配置后,重点不是看它会不会写测试,而是看它会不会主动把“先实现后补测”改成“先测后写”。这是 TDD 真正的分水岭。

3.6 Trae

Trae 里建议把 TDD 当成项目级硬规则使用。触发后只看三件事:有没有失败测试、失败原因对不对、最小实现是不是只够过这一条测试。

3.7 OpenCode

OpenCode 场景下同样适用。你不需要它一次就把所有测试都写完,你只需要它把每一次行为变化都变成一个可重复运行的红绿循环。

3.8 验证 Skill 生效的通用检验法

下面三个提示词,基本能测出这个 Skill 有没有真的生效:

测试 Prompt 1(正向触发):

“给我做一个 Spring Boot 登录注册接口。先写失败测试,再实现代码。”

预期行为:

先给测试用例和失败原因,再给最小实现,不会先铺大段业务代码。

测试 Prompt 2(Bug 复现):

“空邮箱能注册,帮我修复。先写复现测试。”

预期行为:

先产出复现 Bug 的测试,再修代码,最后保留回归用例。

测试 Prompt 3(边界验证):

“我已经写了实现,你直接帮我重构就行。”

预期行为:

Skill 会提醒先确认测试是否存在,必要时要求先补测试再重构。


四、实战案例

“纸上谈 TDD,终究是讲道理;跑起来的测试,才叫证据。”

我们用一个最小的 Spring Boot + MySQL 项目演示:登录、注册、个人中心。

4.1 场景一:开发一个新接口

任务描述: 为用户服务新增邮箱注册、登录、个人中心接口。

技术栈: Spring Boot 3 + Java 17 + Spring JDBC + MySQL 8 + BCrypt + JUnit 5 + Testcontainers

项目骨架
代码语言:java
复制
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
Step 1:先写失败测试
代码语言: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");
  }
}

这一步的意义,不是“写个测试看起来专业”,而是逼出接口边界:接口到底叫什么、返回什么、异常怎么处理,都必须先落到测试里。

Step 2:写最小实现
代码语言:java
复制
@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 在这里反对的不是代码短,而是“把未来可能的需求提前塞进去”。

Step 3:重构

绿色之后再做三件事:

  1. 把 token 生成逻辑从 AuthService 挪到 TokenService
  2. 把异常映射成统一的 @RestControllerAdvice
  3. 把数据库唯一约束和服务层校验对齐,避免双重语义冲突。
Step 4:用 curl 验证闭环
代码语言:bash
复制
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>"

4.2 场景二:修复一个存量 Bug

Bug 描述: 重复邮箱居然还能注册成功。

修复方式: 先写复现测试,再修实现。

代码语言:java
复制
@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() 和数据库唯一约束接上,最后才变绿。这样下次改注册逻辑时,它还能继续替你看门。

4.3 多 Agent 协作场景(可选)

适用条件: 需求稍大、测试较多、需要分工时。

代码语言:java
复制
Orchestrator Agent
├── 负责:拆任务、安排 RED/GREEN/REFACTOR 顺序
├── 使用 Skill:test-driven-development
│
├── Subagent A:写失败测试和复现用例
│   └── 使用 Skill:test-driven-development
│
└── Subagent B:按测试写最小实现并重构
    └── 使用 Skill:test-driven-development

这个分工里,最重要的不是“谁会写代码”,而是“谁先把问题变成可失败的证据”。TDD 让分工边界清晰得多。


五、小结与价值思考

“能跑通,不等于能相信;能复现,才算真修好。”

5.1 软件工程视角

这个 Skill 的本质是: 把“我觉得对”改成“测试证明对”。

从架构师角度:

TDD 解决的是 AI 协作里的信任问题。Agent 可以写得很快,但速度不是质量。真正让团队敢放手的,是它每次都把行为变成测试,把修复变成证据,把重构变成可回退的过程。对团队来说,TDD Skill 更像一条工程纪律,而不是一个编程偏好。

ROI 估算:

场景

引入前

引入后

节省估算

新接口开发

先写实现,返工后补测

先红后绿,路径清楚

少一次大回改

Bug 修复

修完就走,回归靠记忆

复现测试长期保留

少反复踩坑

团队 Review

讨论“像不像对”

讨论“测试有没有覆盖”

评审更快

5.2 发布到你的 GitHub:形成系列资产

把你定制化的 TDD Skill 发布到 GitHub,最实用的不是“我也有个仓库”,而是让团队以后每次做业务都顺着同一套验证顺序走。

1. 创建 skills 仓库

mkdir -p skills/test-driven-development

2. 放入定制化 SKILL.md

cat > skills/test-driven-development/SKILL.md << 'EOF'


name: test-driven-development

description: 先写失败测试,再写最小实现,再重构


EOF

3. 提交并推送

git add skills/test-driven-development/

git commit -m "feat: add tdd skill"

git push origin main

系列索引模板:

期数

Skill

核心价值

安装

#011

test-driven-development

让测试先于实现

npx skills add <owner>/<repo>/test-driven-development


六、衍生思考:下一个 Skill

学完 test-driven-development,你会马上碰到一个新问题:

测试能证明代码行为对了,但页面真的在浏览器里对了吗?

这就是下一期最自然的延伸:webapp-testing

它解决的是从“代码行为正确”走到“真实浏览器路径正确”的那一步。

问题场景: 登录接口单测全绿,但页面上按钮还是失效、样式还是错位、真实跳转还是报错。

下一期预告: webapp-testing — 把“能跑”的感觉,变成“真能用”的证据。


附:方法论速查表

环节

关键动作

工具/命令

注意事项

安装

把 skill 放进可读取目录

npx skills add ...

重启会话后生效

验证

先触发一个新功能任务

对话框 / Goal

先看有没有失败测试

新接口开发

RED → GREEN → REFACTOR

mvn test

不要先写实现

Bug 修复

先复现,再修复

@Test / Testcontainers

复现测试要长期保留

发布

GitHub + README 索引

git push

保持技能可复用


TDD 的本质不是更会写代码,而是更敢相信代码。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景:为什么这个 Skill 很重要
    • 1.1 问题现场
    • 1.2 有 vs 没有 Skill 的差别
    • 1.3 这个 Skill 能解决的核心问题
  • 二、Skill 核心约束:中文解读
    • 2.1 Skill 基本信息
    • 2.2 核心约束思维导图
    • 2.3 规则逐条解读
  • 三、安装指南:各主流 AI IDE
    • 3.1 通用安装命令
    • 3.2 Claude Code
    • 3.3 Codex
    • 3.4 Cursor
    • 3.5 CodeBuddy
    • 3.6 Trae
    • 3.7 OpenCode
    • 3.8 验证 Skill 生效的通用检验法
  • 四、实战案例
    • 4.1 场景一:开发一个新接口
    • 4.2 场景二:修复一个存量 Bug
    • 4.3 多 Agent 协作场景(可选)
  • 五、小结与价值思考
    • 5.1 软件工程视角
    • 5.2 发布到你的 GitHub:形成系列资产
  • 1. 创建 skills 仓库
  • 2. 放入定制化 SKILL.md
  • 3. 提交并推送
    • 六、衍生思考:下一个 Skill
    • 附:方法论速查表
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档