一次发布把下单接口打挂了,CI 里却全是绿的
4 月一次常规发布后,下单接口 5xx 飙到 12%。回看 CI 流水线,单元测试全绿。问题出在:单元测试只覆盖了方法内部逻辑,没人验证"接口契约"本身——前端传的字段名改了,后端字段名也跟着改,但两边没对齐,网关层反序列化直接失败。
这事之后我把接口自动化测试和 CI 质量卡点补齐了。这里记的是落地方式,不是工具清单。
契约测试:在 CI 里锁住接口约定
我们前后端用 OpenAPI(Swagger)定义契约。之前契约是文档,写完就过时。现在用 契约测试把文档变成可执行断言。
用 Spring Cloud Contract 的话,契约文件由后端定义、前端消费:
# src/contracts/order/create.groovy
Contract.make {
request {
method 'POST'
url '/api/order'
body([ userId: 1001, skuId: 88, qty: 2 ])
headers { contentType(applicationJson()) }
}
response {
status 200
body([ orderId: $(anyNumber()), payUrl: $(anyUrl()) ])
headers { contentType(applicationJson()) }
}
}
CI 里跑这个契约,会自动生成对后端实现的测试,校验真实接口返回是否满足约定。前端那边用同一份契约生成 stub,联调时不用等后端起服务。那次字段名不一致的事故,就是契约测试在合并前拦下来的——它发现响应里少了一个 payUrl 字段。
用例管理:别让测试变成一次性代码
很多团队的接口测试写一次就荒废,因为用例散在脚本里、没人维护。我们定了几条规矩:
- 用例跟着接口版本走,放在仓库
src/test/contract,和代码一起评审、一起回滚。 - 覆盖三层:正常路径、边界值(qty=0、qty 超库存)、异常路径(用户不存在返回 4xx)。
- 用统一 base URL 从环境变量读,本地、测试环境、CI 各一套,不把地址写死。
@SpringBootTest(webEnvironment = RANDOM_PORT)
class OrderApiTest {
@Value("${api.base-url}") String base;
@Test
void createOrder_ok() {
given().baseUri(base).header("Content-Type","application/json")
.body("{\"userId\":1001,\"skuId\":88,\"qty\":2}")
.when().post("/api/order")
.then().statusCode(200)
.body("orderId", notNullValue());
}
}
我们用 REST Assured 写这种用例,读起来接近自然语言,新同事也能维护。
流水线质量卡点:不是能跑就行,是卡得住
把测试接进 GitLab CI,关键是卡点设计:
stages:
- test
- contract
- build
- deploy
contract_test:
stage: contract
script:
- mvn verify -Pcontract
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request"'
allow_failure: false # 失败就阻断合并
我设了三道卡点,按成本从低到高排:
| 卡点 | 触发时机 | 失败后果 | 耗时 |
|---|---|---|---|
| 单元测试 | 每次 push | 阻断本次构建 | ~40 s |
| 契约测试 | 合并请求 | 阻断合并 | ~90 s |
| 接口自动化(对测试环境) | 部署后 | 自动回滚 | ~3 min |
最后一道卡点在部署后跑,对刚发到测试环境的实例发真实请求,发现异常就触发 kubectl rollout undo 回滚。我们统计过,接入后"发布导致的线上故障"从每月 4 起降到 1 起。
踩的坑
- 测试环境数据污染。接口测试会真写库,跑完一堆脏数据。后来给测试库加了个 事务回滚:每个用例用
@Transactional包起来,跑完自动滚,环境永远干净。 - 契约只测了后端,前端 stub 没同步更新,联调还是对不上。现在要求契约文件改动必须同时通知两端,CI 里前后端各跑一遍。
下篇预告
这篇先把《接口自动化测试与 CI 集成》里的坑列了,下一篇写我们当时是怎么在线上工程里真正落地的——包括那次让领导拍桌的故障复盘。