API、契约与 WireMock:兼容边界怎样被自动证明
从 HTTP 语义、WireMock 匹配与状态场景推进到消费者契约、提供者回放和兼容发布。
WireMock 3 的 Request Matching 与 Stateful Behaviour 支持协议和状态场景;Pact 的 Provider Verification 要求消费者契约在提供者上回放。
API 测试先固定可观察语义
状态码、Header、Content-Type、字段存在性、空值、错误结构、分页游标、幂等键和副作用共同组成契约。只断言 200 与 JSON 能解析会漏掉缓存语义、错误码和重复提交;把整个 JSON 字符串逐字比较又会把字段顺序、无关扩展和动态时间误当契约。
请求测试同时覆盖方法、路径、查询规范化、鉴权、内容协商、大小限制和无效输入。响应断言区分 required、optional、nullable 与 open enum。增加可选字段通常兼容,删除必需字段通常破坏;枚举新增是否兼容取决于消费者是否有 unknown 分支,不能靠 Schema 名义判断。
WireMock 应模拟协议而不是复制提供者
Stub 对请求匹配越宽,客户端发错 Header 或 body 也会拿到成功;越窄到匹配随机 id、时间和字段顺序,又会产生脆弱失败。匹配客户端真正承诺发送的结构,并验证调用次数、operationId 和禁止的重试。未匹配请求应使测试失败,不能落到万能 200。
状态场景适合第一次返回 202、查询 PENDING、最后 APPROVED,或首次超时后重复提交返回相同结果。每个测试前重置 mapping、request journal 和 scenario state,避免用例顺序决定结果。延迟、断连、畸形响应和大 body 用于验证客户端 deadline、重试预算和资源上限。
消费者契约与 OpenAPI 解决不同问题
OpenAPI 描述提供者允许的整体接口,消费者驱动契约记录某消费者实际需要的交互。Schema 校验能发现类型变化,却不一定证明特定状态、认证和副作用;契约回放在真实提供者上验证请求能被接受且响应满足消费者断言。
消费者发布契约时关联代码版本,提供者在每次变更回放所有仍受支持消费者。Provider state 只准备前置数据,不应 stub 掉被验证业务。契约 Broker 的“可部署”判断还需结合提供者版本、环境和消费版本矩阵,不能只看最新一次绿色。
版本演进需要负向兼容实验
先让读取方容忍新可选字段和 unknown enum,再切写入方;重命名字段采用双写/双读窗口并监控旧字段使用;语义变化即使 Schema 不变也需要新版本或能力协商。错误码、排序、默认值和分页稳定性同样可能破坏消费者。
契约失败时先判断是消费者过度约束、提供者真实破坏还是测试数据失真。为通过门禁而删除断言或让 WireMock 接受任意请求,只会让证据失效。
错误协议比成功样例更容易破坏消费者
消费者常依赖 401/403 区分、409 的重试语义、429 Retry-After、Problem Details 字段和 5xx 是否携带 operationId。提供者只对 200 做 Schema 测试时,异常重构可能把稳定 JSON 变成 HTML 或泄露堆栈。契约为每类可行动失败保留至少一个交互,并验证客户端不会对永久错误重试。
超时测试分别模拟连接建立、首字节、响应体中途和整体 deadline。WireMock fixed delay 只能覆盖一种等待;断连、chunked dribble 与 malformed response 才能验证连接池回收和结果未知。测试断言重试次数、总预算、幂等键复用和最终状态,而不只断言抛出异常。
契约发布也有生命周期
契约关联 consumer version/branch/tag 和 provider state,不能用“latest”覆盖历史。主干提供者验证仍在生产或待发布的消费者集合;功能分支契约与临时环境有清理期限。Pending/WIP 能支持渐进引入,但不能让已承诺契约永久不阻断。
Provider Verification 使用真实路由、序列化、安全和领域逻辑,外部下游可替换;Provider state handler 幂等准备最小数据并在用例间清理。若 state handler 直接构造期望响应或替换被测 service,回放只证明测试桩。
Schema 与语义 diff 需要人工决策
自动 diff 能识别 required、type、format 和 enum 变化,却不知道“金额从分变元”“排序由创建时间改更新时间”或“空数组改 null”。契约用示例和 matcher 固定消费者真正依赖的语义,架构评审处理无法机器表达的变化。不要把所有动态值设为任意字符串,那会删除格式证据。
发布前用 can-i-deploy 类矩阵检查将要部署的精确 consumer/provider 版本,灰度观察解码失败、unknown enum、错误码分布和旧字段使用。回滚时契约也必须支持旧提供者,双向兼容窗口不能只验证前进升级。
先写证明对象,再选择测试层
一条测试记录四件事:要保护的不变量、能制造它失效的最小反例、观察证据的位置、失败后能定位到哪一层。只写“调用成功”没有证明业务;只写工具注解没有说明为何需要这一层。单元、组件、集成、契约和端到端不是等级,而是替身数量、真实边界和反馈成本不同的证据采集点。
测试替身越多,速度越快但与真实运行的距离越大;真实依赖越多,协议证据越强但状态和资源更难隔离。组合策略应让同一高风险结论至少由两种不同失败模式的证据支持,例如单元验证版本条件,真实数据库验证条件更新确实只影响一行。
失败必须保存成可重放状态
失败包至少包含测试 id、代码提交、依赖版本、seed、时钟、Locale、执行顺序、资源标识、容器日志、线程信息和业务见证。结果未知与产品断言失败分开统计;基础设施掉线不能自动归为产品缺陷,产品超时也不能全部甩给 CI。
可维护测试也需要性能、数据和安全边界
套件容量模型包含用例数、上下文/容器启动、数据初始化、并发 worker、日志与制品体积。并行度超过数据库连接、CPU 或容器资源只会增加尾延迟和 flaky;先量测各阶段临界路径,再决定共享、分片或下沉。
演进遵循“读取端先兼容、写入端后切换、旧证据归零再删除”。框架升级先固定当前发现数、执行顺序、上下文 key 和报告格式,再迁移版本;不能把升级造成的少跑测试误当提速。
用两个 Java 17 模型固定测试见证
模型不依赖测试框架或外部服务,只把边界输入、状态转换和期望见证压成确定输出;真实项目再用 JUnit、Mockito、Spring、容器或 WireMock 驱动同一不变量。
javac --release 17 -Xlint:all -Werror examples/backend-development/testing/api-contract-wiremock/ContractCompatibilityDemo.java examples/backend-development/testing/api-contract-wiremock/StatefulStubDemo.java
java -cp examples/backend-development/testing/api-contract-wiremock ContractCompatibilityDemo
java -cp examples/backend-development/testing/api-contract-wiremock StatefulStubDemoaddedOptionalField=true removedRequiredField=false enumExpandedSafely=false consumerCompatible=true
scenario=payment states=[STARTED, PENDING, APPROVED] requests=3 resetVerified=true实际测试若改变输出,必须解释是业务规格、测试层、Fixture、调度还是门禁发生变化;禁止用放宽断言或无限重试吸收差异。
从失败现场回推测试体系缺口
契约只保护消费者实际依赖的语义,并必须由真实提供者回放验证。围绕这条不变量记录测试 owner、独特证据、运行成本、失败率和最近一次真实发现。长期从不失败且与其他用例完全重复的测试需要合并;只在生产才失败的关键路径需要升级替身或增加真实边界。测试资产和生产代码一样接受删除、重构与版本治理。
团队门禁拒绝没有证据的绿色
代码评审要求说明为何选这一层、替身与真实系统差异、数据如何隔离、失败如何重放和哪个发布风险被覆盖。CI 报告展示未运行、跳过、隔离与重试,不允许只汇总“最终通过”。
测试资产的容量、升级与恢复
围绕“契约只保护消费者实际依赖的语义,并必须由真实提供者回放验证”建立资产台账:用例 owner、被保护规则、依赖资源、平均/p95 耗时、失败率、独特证据和最后发现缺陷。套件增长时按临界路径和资源饱和定位,而不是只看用例总数。共享容器降低启动成本但提高污染半径,并行 worker 提升吞吐但会争用数据库、CPU 和端口;每次提速都用隔离失败率与反馈时间共同验收。
框架、JDK、数据库或 Broker 升级前保存测试发现数、跳过数、tag、Context key、容器镜像、契约矩阵与关键见证输出。先让测试基础设施兼容新旧两套,再切被测版本,最后删除旧分支。升级后“跑得更快”若伴随测试数下降、Vintage engine 消失或条件测试被跳过,不是优化。
最终验收不以一次全绿结束:在相同 seed、调度、版本和资源边界下重复关键反例,确认失败能稳定出现、修复能稳定消除,且无关重构不会破坏测试。
