Files
lixi 222990e587
CI / docs-build (push) Successful in 1m19s
docs: M2 第二波收口——报告 13~21 与契约草案入档挂导航
- 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>
2026-09-08 10:40:27 +08:00

70 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 test` **182 项全绿**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 快照同步纪律
1. **正典唯一**:契约的唯一权威是 doc 仓 `docs/api/openapi.yaml`;api 仓快照是冻结副本,**永不单独修改**。
2. **契约变更流程**:doc 仓升版(如 1.3.0)→ 复制新文件为 `src/test/resources/contract/openapi-v1.3.0.yaml`(删旧快照)→ 更新 `OpenApiContract.RESOURCE` 与守卫测试期望值(版本号、路径/操作/schema 数)→ 按新契约增删测试用例,一并提交。
3. **忘同步的兜底**:守卫测试 `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/4010111 个 pet 路径操作验 40401 防枚举、3 个顶层短路径验 40402、8 个写操作按 viewer/caregiver 角色验 4030012 处 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**ContractConformanceTest6 成功形态 + 3 错误信封 + 2 门禁) |
| **合计** | **171** | **182** | **+11** |
`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`BUILD SUCCESS182 项 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,确定性触发守卫落空;本单未做(属行为构造技巧,优先级低)。