实践3|用 Claude Code 从需求文档走到集成测试打通真数据库

上一次实践只做了一件事:改一个工具类,加 mock 测试,再补一个真接口打通的集成测试。这次链路长很多------从"读需求文档"开始,走到"跑通连真数据库的集成测试",全程扔给 Claude Code。

这一篇有意识地把 TDD 作为主线走了一遍,需求消化这个上游动作也换了个玩法:让 AI 先把需求写成一份可回溯的 md,再切新会话开工

一、读需求:让 AI 先替我做一份结构化笔记

需求来自公司协同文档平台上一个需求单页面。我通常的做法是打开浏览器、复制文字、边看边写代码。这次换了个玩法。

第一步就一句话:

打开这个需求单链接,理解需求,生成一份 md 文档,放到 <某目录>/RRR_xxxx_<需求名>_<需求号>.md,用需求名当文件名。

它做的事:

  1. 拉页面正文
  2. 提取"需求原文 / 成功标准 / 待确认问题 / 技术方案 / 实施步骤 / 相关成员"这些段落
  3. 在"待确认问题"里主动列了 6 条:口径定义、userId 到底指哪个、批量导入是否原子、上限值是不是随用户等级变化、错误码规范、现有接口清单
  4. 落成一份完整 md,还带 checklist 勾选框

关键在第 3 步------它没有直接开始"实施步骤",而是先把我看需求时会漏问的东西列出来了。这些问题里有几个我是真没想到(比如"userId 是登录用户 id 还是被保用户 id",两者在替家人签约的场景下不一致)。

写完 md 我 Task 列表里加了一条 TDD 第 1 步:写单测(失败),把 md 文档路径贴进去,切另一个新会话开工。

这个动作看上去只是"把需求整理了一遍",隐藏收益有两个。一是 AI 主动列出的待确认清单 ,比我自己看需求时的清单更全------我看需求会带着自己的假设跳读,它是无假设通读。二是这份 md 成为会话之间的桥:下一节会讲,切到新会话开工时,我不用再复述需求,一句"按这份 md 实现"就够。

二、复用上一次逆向出来的文档

新会话里我说:

请按 TDD 方式,完成 RRR_xxxx_<需求名>.md(在上级目录)这个需求

它做的第一件事不是搜代码------是先去读 docs/<模块名>.md。那份文档是实践1里我让它逆向陌生业务模块生成的,包含时序图、错误码对照、代码位置速查。

于是它 30 秒之内就定位到:

  • 校验应该塞在哪个 Controller 的哪个方法里(有一段 FIXME 不应该写在这里,待重构 的注释,我这次不管)
  • 从当前应用层走到权威数据服务的完整链路穿了 4 个模块
  • 表结构 / 字段口径都能在文档里直接查到

实践1 存下来的那份逆向文档,在实践3 里回收利益了。 不是每次会话都从零开始摸索,前面的会话产出真的能被后面复用。

三、TDD 主线,但不是纯洁的 TDD

需求确认好,动手前我先让它建分支------4 个要动的子仓库,都从 prod 拉 feature/xxx-260709。这里踩了个小坑:脚本默认会 push 到远程,但我本机到内网 gitlab 网络不通,git fetch 卡了一分多钟才超时。后面直接从本地已有的 origin/prod ref 建分支(不 fetch),跳过网络。

然后主线是 TDD 的 6 步:

动作 预期状态
1 写应用层的 Mockito 单测(校验器 3 个用例:低于阈值 / 等于阈值 / 高于阈值)
2 连接层加转发方法 编译过
3 开放接口层加转发方法 编译过
4 权威域服务加 mapper SQL + Service 方法 编译过
5 应用层校验器把逻辑填进去、Controller 接入调用 红→绿
6 跑单测 3/3 绿

严格 TDD 说"先写测试到编译不过为止"。我没这么干。 我先写了目标类和方法的空骨架(方法体是 // TODO),让测试能编译过、但运行时全红。这样比"红到编译错误"更容易看出哪个断言在期望什么。

跑第一遍单测的时候 3 个用例全红,红得很干净------两个 "should throw" 的因为空实现不抛,一个 "should pass" 的因为空实现根本没调 Feign(Wanted but not invoked 是这个测试给我的信号)。

补完 4 层链路 + 应用层校验器逻辑,再跑一遍,3/3 绿。

过程里最有价值的不是"最后绿了",是中间那次红。 红的形状告诉我:Mock 桩打对了、参数捕获断言写对了,缺的就是校验器本身的实现。后来补实现的时候我几乎没有犹豫,直接对着"要让哪个断言过"写代码。

四、几次被我打断、被我改口的地方

TDD 主线走的时候,几个交互点值得单独记:

4.1 请求体最好是 Bean,不是裸参数

我看到它给 Feign 接口的方法签名是这样的:

java 复制代码
Result<Long> countActiveWarrantyByUserId(@RequestBody Long userId);

Long 当请求体在技术上没问题,但语义上没扩展性。我打断它:

请求体最好用 bean,不要这样,就算他现在只有 1 个类型

它改成一个 CountXxxByUserReqDto,只带一个 userId 字段 + @NotNull 校验。以后要加"上限值"、"来源标记"字段的时候不用改签名。

这个不是 AI 犯错,是技术选型的权衡。它选了简洁,我选了未来可扩展。这类交互不该指望它"自动做对"------它连"未来"是什么都不知道。

4.2 日志规范

它写日志的时候套用了老代码风格:

java 复制代码
log.info("【某某上限-Connector】统计...,userId={}", ...);

我拦下:

【】这种前缀不要用,日志系统分词分不出来;日志默认 debug + isDebugEnabled 守卫;info/warn/error 只留给关键节点和异常路径;旧代码不要动。

它同时改了新写的 8 处日志,把 【某某上限-XX】 换成中文短语(某某上限 适配层 userId={}),把 info 全部降到 debug 并加守卫,只保留 warn(异常返回)和 error(外部依赖失败)用于关键路径。旧代码保留不动------外科手术式改动。

这条我让它写进了 memory。下次它写日志会自动这么干。

4.3 单测不要用实时数据快照做断言

后面做集成测试之前,我用命令行连测试库跑了一句 SQL,看到目标 userId 对应的记录数是 56。我随口一提,让它加个"count=56 时抛异常"的用例。

它加了。加完我觉得不对------单测已经有"count=4 通过 / count=5 抛 / count=6 抛"三个覆盖了等价类的用例,再加 count=56 是同类重复,而且 56 是当前数据库快照,明天数据被清就没意义了。

我让它删掉那个用例,并把这条也写进 memory:

单元测试的断言值来自等价类划分(低于阈值 / 等于阈值 / 高于阈值),不吃数据库真值;数据库真值只用于开发期人工验证,且只在测试环境直连查。

这个错误值得记,它伪装成"更真实的测试",实际是"锚定当下数据"的伪保障。哪天把上限从 5 改成 100,前 3 个测试会正确变红,那个 count=56 的用例却依然是绿的------形成误导。

五、想让集成测试真的连数据库

Mockito 单测的问题在于:它证明"校验器逻辑对",但没有证明"我新写的 SQL 语句本身对"。SQL 里字段名拼错、resultType 写成 Integer、XML 里 id 和 Mapper 方法名对不上------单测全部感知不到。

想做集成测试。要求:

  1. 真连测试库跑 mapper(不走 Mock)
  2. 只在显式加参数时运行(默认 mvn test 不能触发,避免 CI 挂)
  3. 敏感的 DB 明文密码不能进 git

它先按我要求搭了骨架:

  • 极简 MapperTestApp 只装配 DataSource + MyBatis-Plus(这是关键,因为完整启动会拉一堆用不到的外部服务)
  • application-inttest.yml 直接写死测试库 URL / 用户 / 密码
  • XxxMapperIT 两个用例:真实 userId 断言 count > 5不是 = 56 )、不存在的 userId 断言 count = 0
  • Maven pom 加 profile:默认 surefire 排除 *IT.java-Ptest 反向包含

注意:断言只写 > 5,不写 = 56 这就是上一节踩过的坑,AI 记住了。

第一次跑起不来,报错是加载 bootstrap 阶段解析不到配置中心占位符。第二次跑起不来,报错是某个 auto-config 的 @Component 找不到依赖 bean。改了两轮:

  1. 覆盖一份空的 bootstrap.yml 到 test resources,禁配置中心 placeholder 解析
  2. MapperTestApp 从"扫所有 auto-config"改成 @ImportAutoConfiguration 白名单------只显式装 4 个:PropertyPlaceholderAutoConfiguration / DataSourceAutoConfiguration / DataSourceTransactionManagerAutoConfiguration / MybatisPlusAutoConfiguration

跑通。日志里能看到 MyBatis 打印的真实 SQL:

vbnet 复制代码
SELECT COUNT(*) FROM t_xxx WHERE user_id = ? AND status = 1 AND deleted = 0
Parameters: <真实的 userId>(Long)
<==      Total: 1

2/2 绿。这次是 SQL 真的执行了、字段名真的对上了、参数绑定真的走通了,不是 Mock 兜住的结果。

但这个做法其实不够"工程化"

写完我心里知道一件事:这次是图省事的做法。

用的是"读测试库里的历史真值"------那个测试库里恰好已经有一个 count 远超 5 的用户,直接拿它验证 > 5 就成立了。这个玩法有几个隐性代价:

  • 依赖测试库的历史状态。哪天 DBA 清库、或者那批历史数据被别人删了,测试就红了------红的原因跟我这次的改动毫无关系
  • 只读、不写------覆盖不到 insert / update / 唯一索引冲突这类需要造数据才能触发的路径
  • 多人同时跑测试,可能互相踩到对方留下的痕迹

更工程化的做法应该是这样:

java 复制代码
@SpringBootTest
@Transactional        // 每个测试方法都在一个事务里
@Rollback             // 结束时自动回滚,不留脏数据
class XxxMapperIT {

    @Test
    void countActiveByUserId_withThreeActiveRows_returnsThree() {
        // given: 当场造 3 条 status=1 的记录
        Long userId = uniqueUserId();  // 用当前时间戳等造一个不冲突的 id
        insertActiveRow(userId);
        insertActiveRow(userId);
        insertActiveRow(userId);
        // 再造 1 条 status=2 的(不应该被 count 到)
        insertTerminatedRow(userId);

        // when
        long count = mapper.countActiveByUserId(userId);

        // then
        assertThat(count).isEqualTo(3L);   // 精确断言,不再是 > 5
    }
}

好处一下就出来了:

  1. 断言可以精确到具体数字,数据是我这次刚造的,能算出来
  2. 测试自包含------不依赖测试库里有没有历史数据,任何人任何时候跑都是同一份行为
  3. 顺带把状态过滤逻辑也覆盖了 ------造一条 status=2 的看它有没有被漏过去。这才是 SQL 里 AND status = 1 这一句真正的守门测试
  4. 事务回滚意味着测试跑完什么都不留,多人并行、CI 反复跑都不会互相污染

为什么这次没这么干? 三个原因,按诚实程度排序:

  • 目标数据在测试库里"现成",走了懒路
  • insert / update 的样例数据造起来不省事------t_xxx 有几十个字段,很多都是 NOT NULL,得挨个填充或者写个 builder,一次性成本比"读现成的"高
  • 我的初衷只是"验证一下新加的 count SQL 语句本身没写错"------验证目标很窄,读一次真值也够用

从"以后要沉淀成一套 IT 骨架"的角度看,@Transactional + @Rollback + 数据构造器才是那条应该走的路。我这次相当于给别人挖了个坑:下一个人写这个模块的 IT 时,会照着我的样例继续读真值,直到某一天数据被清、大家一起红。

这一段单独记下来,是想说:能跑绿的测试 ≠ 好测试。这次跑绿的是"我",不是"这套 SQL"。下次做同类事,得让绿的是"SQL 本身"。

六、明文密码不能进 git

搭好之后我发现一个尴尬事:application-inttest.yml 里有测试库明文密码,如果 commit 就进 git 历史了。虽然这个库是内部测试库,但明文密码进 git 依然是坏习惯。

我提了个改造要求:

改名 application-my.yml,加到 .gitignore,同时说明下必须自建才能跑用例

它做了几件事:

  1. application-my.yml 变成本地个人配置,加进 .gitignore
  2. application-my.yml.template 是能进 git 的模板,敏感字段用 <TEST_DB_HOST> 之类占位
  3. 测试类 Javadoc 里明确写运行前置 :必须先复制模板→改成 application-my.yml→填真实值,才能跑
  4. @ActiveProfiles("my") ------ 后来发现商城几个模块的测试基类已经用 @ActiveProfiles({"dev", "local", "my"}) 的惯例了,"my" 是团队约定的"本地个人配置"占位。我这次相当于把这个既有约定引入到主业务后端来。

未创建 application-my.yml 时会发生什么?Spring Boot 起不来(找不到 spring.datasource.url)→ 测试报错而不是绿过。报错信息足够明确,看到就知道要改配置。"缺失即失败"比"缺失时静默跳过"更好------前者你一定会补配置,后者可能永远发现不了这段测试根本没跑。

七、这次学到的几件事

按重要程度排:

1. 需求先落成 md,会话之间有桥。 上一节讲到的"实践1 逆向文档在实践3 被回收",是同一个道理的另一面。让 AI 生成一份可回溯的文档制品,比让它"记住某个会话里说过的东西"稳固得多。会话会过期,md 不会。

2. 骨架能编译但为空 + 测试全红,比"编译错到红"更好。 严格 TDD 的定义可能不同意,但从"看红色形状定位缺什么"这个实际目的看,骨架式起点更实用。红的形状要说人话:告诉我"Mock 期望被调但没调"比"编译错找不到符号"信息量大得多。

3. 单测断言用等价类,不用数据库真值。 56 那个案例是最好的教训。数据库快照做单测断言就是伪保障。

4. 集成测试要真的碰边界,但断言的数据得是自己造的。 花了 6 个小时搭这套 IT,是为了确认 SQL 语句本身、字段名、参数绑定这些Mock 兜不住的东西 。这个投入的回报要等到某次半夜生产报错时才能变现------那时候你的 mapper 已经在集成测试里跑过 100 遍,可以放心排除它。但这次我在断言数据上偷了懒------读了历史真值而没有自己造数据 + 事务回滚 (第五节最后一段有反省)。工程化的 IT 骨架应该是 @Transactional + @Rollback + 数据构造器,让每个测试自包含、精确断言、跑完不留脏。这次没做,下次做同类事得补上。

5. 敏感配置走"模板 + gitignore + 运行时报错提示"三件套。任何单件都不够:只有模板没 gitignore,第一天就漏;只 gitignore 不给模板,后来的人不知道怎么起;两个都有但没在测试类里说明前置条件,别人跑不起来会以为代码错了。

6. Memory 里存"约定"而不是"事实"。 这次新加了两条:日志规范、单测不用实时快照。以前存的三段式测试注释规范、Maven settings 路径也在持续生效。都是"约定"性质------每次都可以套用,不会因为项目状态变化而失效。相反,"某某表的 count=56" 就是"事实",不该存。

时间账

阶段 手工 这次
需求消化 + 澄清疑问 30 min(+ 找产品 QA) 15 min(AI 主动列出待确认清单)
定位改动点 20 min 3 min(复用了实践1 的逆向文档)
4 层链路改代码 + 单测 90 min 40 min
集成测试搭建 从没搭过 30 min(含 2 次报错排查)
明文密码整改 5 min(大概率会忘) 8 min(一次做全)

链路长了,但单点省下的时间也多了,总耗时反而下降。真正的杠杆不在"AI 敲得快"------是在记得住约定记得住上次生成过什么文档可以复用能在没被要求的时候主动列出问题清单

相关推荐
冬奇Lab2 小时前
开源项目第157期:archify — 用自然语言生成可主题切换、高分辨率导出的架构图
人工智能·架构·claude
码哥字节1 天前
别再纠结 24 万星 Superpowers 还是 5.8 万星 OpenSpec,有人把他们融合成一个工作流在 Github 开源
ai编程·claude
武子康1 天前
OpenAI Workspace Agents 进入信用计费:Coding Agent 从额度体验走向成本治理
人工智能·openai·claude
LYFlied2 天前
Anthropic Agent隔离架构解读:比权限弹窗更可靠的是限制爆炸半径
人工智能·架构·软件工程·claude·安全架构·ai agent
凌奕2 天前
Claude Code 上线了"一键发网页"功能:报告做完直接甩链接,部署这一步彻底消失
html·claude
ServBay2 天前
你还在手写 Prompt?聪明的人早就用上了循环工程,AI 的自动驾驶时代来了
ai编程·claude·mcp
故乡de云2 天前
gemini-3-pro-preview 调用 generateContent 完整配置与排错:REST / Python SDK / Code0 兼容接入
人工智能·大模型·api·claude
sg_knight2 天前
Claude Code 安装指南(macOS / Linux / Windows WSL)
linux·windows·macos·claude·code·anthropic·claude-code