- 13~18 后端纵切六单报告(T2-03~08,测试 95→182) - 14 + openapi-pets-draft.yaml 契约起草档案 - 19 契约冻结报告(v1.2.0,22 项草案修正对照) - 20 契约一致性测试(快照机制 + 1 漂移修复) - 21 第二波收口总表(定型语义汇总,第三波接入依据) - mkdocs build --strict 通过 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.3 KiB
20 · T2-09 契约测试报告:实现与冻结契约 v1.2.0 的一致性保障
- 日期:2026-09-08
- 角色:Senior Developer(后端)
- 工单:T2-09 验收的契约一致性保障
- 代码提交:patbond-api dev
d026f2f(基线00f7dbd) - 结论:pets 域 18 操作全矩阵契约测试落地并入 CI(
./mvnw test即自动执行,ci.yml 零改动);发现并修复漂移 1 项;全套./mvnw clean test182 项全绿(171 → 182,+11)。
1. 机制选型:冻结快照进测试资源
选定方案:把 doc 仓正典 docs/api/openapi.yaml(v1.2.0,冻结于 doc main@511617b)字节级复制为 patbond-api 测试资源 patbond-pet/src/test/resources/contract/openapi-v1.2.0.yaml,契约测试对照快照跑。复制时点双方 sha256 均为 243fe648…4a4cd689d。
否决的备选:CI 里 checkout doc 仓再喂给测试。现有 ci.yml 是零外部 action、手动 git init + fetch 克隆本 Gitea 实例的模式,跨仓 checkout 意味着在工作流里再造一段带 token 的手动克隆、并让本地 ./mvnw test 依赖兄弟目录存在——本地与 CI 行为分叉,违背「门禁与本地同一条命令」的既定纪律。快照方案零 CI 改动、本地 CI 完全同构,代价只是一条同步纪律(见 §1.2)。
解析与校验实现:不引 swagger-parser / openapi-validator 类库——快照只用到 OpenAPI 3.0 的一个小子集(本地 $ref、type/required/nullable/enum/format/min-max),用构建里已有的 snakeyaml(Boot 传递依赖)解析 + 自写严格断言(约 500 行测试代码),零新增 Maven 依赖。自写的关键收益:未声明字段即报漂移——标准 OpenAPI 语义默认允许 additionalProperties,而冻结契约的语义是「恰好这些字段」,现成校验器恰恰放过改名/新增泄漏字段这类最常见漂移。
1.1 三个测试类
文件(均在 patbond-pet/src/test/java/...pet/contract/) |
职责 |
|---|---|
OpenApiContract |
加载快照、解析本地 $ref、枚举操作/状态码/schema |
ContractValidator |
响应体对 schema 严格校验:必填缺失、null 无 nullable、契约未声明的字段、类型/枚举/uuid/date-time/date 格式、min/max(Length) 边界 |
ContractConformanceTest |
沿用既有 Testcontainers + MockMvc 基建真实起服务,18 操作逐一发请求校验,最后两个门禁测试(见 §2) |
1.2 快照同步纪律
- 正典唯一:契约的唯一权威是 doc 仓
docs/api/openapi.yaml;api 仓快照是冻结副本,永不单独修改。 - 契约变更流程:doc 仓升版(如 1.3.0)→ 复制新文件为
src/test/resources/contract/openapi-v1.3.0.yaml(删旧快照)→ 更新OpenApiContract.RESOURCE与守卫测试期望值(版本号、路径/操作/schema 数)→ 按新契约增删测试用例,一并提交。 - 忘同步的兜底:守卫测试
frozenSnapshotIsTheExpectedContractVersion锁定info.version == 1.2.0且 18 路径 / 24 操作 / 45 schema——契约变更后只改快照不改测试(或反之)都会在 CI 立即变红,不会默默对着旧契约测试。
2. 测试什么:全响应矩阵 + 双门禁
覆盖 pets 域 18 个操作(契约中 tags ∈ {pets, dictionaries, health-records} 的全部操作,恰为 v1.2.0 新冻结的 12 路径)。每个操作真实发请求,对契约声明的每一个 (操作, 状态码) 单元格做结构校验:
- 成功形态(6 个用例):宠物 CRUD 全字段/全空两种形态、品种与疫苗目录(含 species 过滤)、体重与健康事件的 cursor 分页翻页(并断言
hasMore=true ⇒ nextCursor 非空、hasMore=false ⇒ nextCursor 恒 null)、疫苗 scheduled/completed 两形态与状态机 PATCH、提醒 completed/dismissed 两种流转、摘要空档案(三聚合 null)与满档案(四聚合非 null)+ tz 参数。 - 错误信封(3 个用例):18 操作逐一裸请求验 401/40101;11 个 pet 路径操作验 40401 防枚举、3 个顶层短路径验 40402、8 个写操作按 viewer/caregiver 角色验 40300;12 处 400/40000(缺必填、limit 越界、非法 cursor、非法 species/status/tz)、40902 乐观锁过期(pets/vaccinations/health-events 三处)、40903 芯片号冲突、40904 剂次冲突、42201 疫苗规则两形态、42202 提醒规则。
- 门禁一(快照守卫):见 §1.2 第 3 条。
- 门禁二(覆盖率自证):
everyDeclaredResponseCellIsExercised断言上述用例真实触发并通过校验了契约声明的每一个响应单元格——契约将来新增操作或状态码,此测试自动变红,覆盖不会静默滑坡。唯一豁免:PATCH /care-reminders/{id}的 409(无 version 列,靠并发条件更新守卫落空触发,单线程 MockMvc 无法确定性构造;其行为语义由第一波并发一致性设计与集成测试背书)。
行为语义(状态机迁移合法性、防枚举响应一致性、权限矩阵、幂等键语义)不在本单重复——既有 78 项 pet 集成测试已锁定,本单只锁结构。
有效性自证(mutation check,未入库):向快照 Pet schema 注入假必填字段 bogusDriftField 后跑测试,9/11 用例即刻红($.data.bogusDriftField: 契约必填字段缺失);还原快照后全绿。校验器确实在咬合,不是恒真。
3. 发现并修复的漂移
| # | 位置 | 契约 | 实现(修复前) | 定性与处理 |
|---|---|---|---|---|
| 1 | POST /api/v1/pets 请求体 sex |
CreatePetRequest.required 含 sex |
sex 可缺席,服务端静默补 unknown |
结构性漂移,按「以冻结契约为准」修实现:CreatePetRequest.sex 加 @NotBlank(缺失 400/40000),PetService 移除缺省补值;7 个既有测试文件的创建载荷补 sex 字段 |
仅此 1 项。其余 17 个操作的请求必填、响应字段名/类型/nullable、错误码值与冻结契约零偏差——第二波「先定型实测行为、再按行为冻结契约」的流程有效。无语义级冲突,无需仲裁项。
4. 测试数变化
| 模块 | 之前 | 之后 | 变化 |
|---|---|---|---|
| patbond-common | 3 | 3 | — |
| patbond-user | 59 | 59 | — |
| patbond-auth | 31 | 31 | — |
| patbond-pet | 78 | 89 | +11(ContractConformanceTest:6 成功形态 + 3 错误信封 + 2 门禁) |
| 合计 | 171 | 182 | +11 |
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test:BUILD SUCCESS,182 项 0 失败。CI 无需任何改动——契约测试就是普通 surefire 测试,./mvnw -B clean test 门禁自动携带。
5. 范围外记录
- auth 域 6 操作无契约测试(register/login/refresh/logout/me/trackEvents):M1 交付时无此机制,本单按工单口径不补,建议 M2 内另立工单——机制已就绪(快照已含 auth 域全部 schema,
OpenApiContract/ContractValidator直接复用),估计半天以内,落在 patbond-auth 与 patbond-user 的测试模块。 - 提醒 PATCH 409 豁免:如后续想消除唯一豁免,可在测试中直接 UPDATE 数据库把提醒改成终态后再以旧状态提交 PATCH,确定性触发守卫落空;本单未做(属行为构造技巧,优先级低)。