实践2|用 Claude Code 跑完一次"看文档 → 改代码 → 写测试 → 打真接口"
这次做的事很小:改一个工具类的三个接口,根据上游新加的一个字段,然后补测试。以前会觉得这种活不值得写笔记——不就是加个字段嘛。但从"看接口文档"到"打通真接口"整个链路我全程扔给 Claude Code 做,过程里踩到几个小坑、也顺手做对了几件事,值得记一下。
写在最前:这次没上什么高阶手法(TDD、并行 agent、workflow 都没用),就是最普通的一次人机结对——我描述、它执行、我盯着。系列越往后我会挑更折腾的场景,这一篇当基线。
背景
有个工具类,作用是调外部系统的三个查询接口(一个查一级分类、一个查二级分类、一个查三级分类的绑定列表)。上游改了:其中一个接口的请求体加了个新字段,Apifox 上文档已经更新,需要我这边跟着调整并加测试覆盖。
一、看文档、改代码、写 mock 测试
第一步就一句话给它:"打开 Apifox 上这个链接,对比一下现在代码里的请求参数,看看要改哪。"
它做的事很朴素:拉 Apifox 页面 → grep 到项目里对应的请求 DTO 类 → 逐字段比对 → 指出新字段位置。省了我十几分钟"人肉对字段"的功夫。
改代码更简单:在 DTO 里加一个字段,把工具类方法里透传一下。它顺手把注释也补齐了。
mock 测试是它主动提出来加的。我没要求,它写完代码就说"要不要顺便写几个 mock 测试覆盖一下改动"。这里有两件事值得说:
第一,我在会话里放了一条 memory:单元测试必须写成 // given / // when / // then 三段式,@DisplayName 用中文。这条是我以前会话里跟它约好的,之后每次它写测试都自动遵守。约定一次、终身生效——这个机制我在上一次实践里也用过,存的是"这份文档在哪里";这次存的是"测试要长什么样"。
第二,测试里的 URL 我一开始让它用真实的内网域名占位。写完我意识到没必要——测试用假域名就够了。它按我说的改了,然后想把"URL 占位统一用某个域名"这条也写进 memory 长期遵守,我拦了一下:这属于一次性提醒,不该占 memory 位置。它就撤回了。
memory 这东西挺容易膨胀,得像整理书桌一样,不停问自己:"这条下次真的会用到吗?"能答"是"的才留。
三条 mock 测试写完全绿,改动闭环。到这里其实可以收工了——CI 会跑、代码 review 也能过。但我想再加一层,因为 mock 有个它兜不住的盲区。
二、mock 不能兜的那件事
mock 测试盯的是"我们这边的代码":请求体拼装对不对、拿到某个响应会不会解析、异常路径有没有兜底。它假设外部服务返回一切都符合契约。
但如果外部接口自己变了——URL 改了、鉴权规则变了、字段名偷偷换了——mock 完全看不见。我这次改的就是"跟上上游变化",那我就更需要一个东西直接打真实接口,确认"我理解的契约=服务端实际返回的契约"。
所以想再加一层集成测试:真的换一次 token、真的打这三个接口。这类测试不能进 CI(外部服务挂了 CI 就红),但需要一个能随时手动跑的入口。
三、让它先问,别让它先写
第二版需求我给得依然很短:
除了 mock 测试的,我想要真的流程测试,这个地址已经是对的了,包括 token,还有 given 我可以提供一些参数。
看上去信息给全了:地址对、token 有、参数占位。要是让它直接动手,估计会写出一个能编译但完全跑不通的骨架。
它这次没写,先问了三个问题:
- Token 怎么给? 走标准的 OAuth2 客户端凭证换 token,还是我直接塞一个 access_token 进去?
- 三个接口都要真调吗? 最小分页各一条,还是只挑其中一个重点验证?
- 测试类怎么跑? 命名成
*IT.java+ 环境变量触发(这样mvn test不会误跑),还是塞进现有测试类硬编码 token,还是@Disabled手动开关?
这几个问题问得挺关键。特别是第三条——我一开始压根没想过测试类命名的事,如果它自作主张塞进原来的测试类,token 或者内网地址就会跟着提交进 git 历史,后面再想清理挺麻烦。
让它先摆选项,我 5 秒钟就能选完;让它自己选,它多半挑最省事的那条走,结果得返工。让 AI 主动多问短期看更慢(多一轮往返),但长期省的是"看错了返工"的成本。这次三个问题两分钟就答完,如果没问,回头发现 token 硬编码进 *Test.java、被 mvn test 自动扫到、CI 挂了,那就是一小时起步的清理工作。
四、命名即隔离
最后落成的文件命名以 IT 结尾(Integration Test)。Maven Surefire 默认只扫 *Test、Test*、*Tests,*IT 会被跳过——它是留给 Failsafe 插件用的。
这个约定看似不起眼,但它把两件事在物理层面隔开了:
*Test.java:跑mvn test就会执行。全部是 mock 或纯逻辑,不依赖任何外部服务,永远秒过。*IT.java:mvn test不动它。要跑就显式-Dtest=xxxIT手动触发。真实调外网,可能挂。
之前我脑子里的方案是给方法加 @Disabled 手动开关。但那种方案的问题是——测试类还是会被扫到,任何时候有人开开关关就可能提交出去。命名约定是编译期就决定了的隔离,比运行时开关更牢靠。
这一条是 AI 反过来教我的。我并没有意识到 *IT 这个约定的存在,是它列选项时提到的。
五、失败的测试也能是成功的探测
写完第一版,跑起来三个测试全红。看错误日志的瞬间我笑了——它们其实是绿的。
失败信息里贴着接口真实返回的报文:
业务失败, resp={"status":0,"message":"成功","data":{...}}
status:0 表示接口调用成功,message:"成功",data 里有真实的分页数据。三个接口全都返回了正常业务数据,token 也换到了。整个链路完全通。
失败的是我的断言——我按经验写了 body.code == 0,但这个平台顶层用的是 status 字段,不是 code。把断言改一改,重跑,三条全绿。
第一版测试真正的价值往往不在"它通过",而在"它能把服务端的实际返回打印出来"。契约文档写得再详细也可能过期,真实的响应报文才是最可靠的字段字典。
如果我一开始就为了让它"通过",写一个 assertNotNull(body) 就交差——那就学不到 status/message/data 这个真实结构。带着有点风险的断言写出去、看它以什么样式失败,其实是探测契约的一种手段。
六、mock 和 IT 断言必须不同源
回过头看,mock 测试和 IT 测试的断言路径必须不同源,才有互相印证的价值。
mock 测试的断言可以贴着代码写:
assertTrue(result.successFlag(), "...");
successFlag() 是我们自己代码里的方法。
IT 里我故意不用它,直接读原始报文:
Integer status = JSONUtil.parseObj(body).getInt("status");
assertTrue(status != null && status == 0, ...);
因为 IT 就是要独立于"我们这边的代码"验证接口本身——如果 successFlag() 有 bug,mock 测试和 IT 会一起错,那就白测了。
以前很少同时写这两种测试,这个区别其实是被 AI 一步步逼着做选择、当场想明白的。
七、给参数留个坑
需求里我说"given 我可以提供一些参数"。它落地的方式是:三个测试方法里 given 段都只填最小分页参数(页码、页大小),然后写一行注释:
// 需要按名称 / 编码筛选时在这里补参数,例如:
// .someName("...")
// .someCodes(List.of("..."))
XxxRequest req = XxxRequest.builder()
.pageNum(1)
.pageSize(10)
.build();
留占位是一件小事,但它决定了这个测试的使用频率。下次线上说"某个字段查不出来",我不用重新想怎么写测试——打开这个类,注释掉的那行取消掉、填参数、点跑。1 分钟能复现一个真实链路的场景。
一个好用的测试类,往往不是"一次性把所有场景都写全了",而是骨架搭好、具体场景 5 秒钟能加一个。
时间账
从"打开 Apifox 看文档"到"真接口打通",全程一个会话搞完。放在以前这活的时间账大概是:
| 阶段 | 手工 | 这次 |
|---|---|---|
| 对文档 / 定位改动位置 | 15 min | 2 min |
| 改代码 | 5 min | 2 min |
| 写 mock 测试 | 20 min | 3 min |
| 写真实链路测试 | 40 min | 15 min(含来回问答) |
真正省时间的不是"AI 敲得快",而是"AI 记得住"——三段式测试规范、Maven 命令要带的 settings 参数,这些以前每次都要我重复告知的东西,memory 里存一次就永远遵守。