合同测试
1)合同在哪里适用
HTTP REST/JSON:资源、分页、过滤器、等效性、错误代码。
gRPC/Protobuf:消息类型,状态,"deadline"语义,backward-compat v.proto。
GraphQL:电路,非空格,指令,对字段进行冲洗。
消息/流(Kafka/Pulsar/SQS):事件方案(Avro/JSON/Protobuf),分期密钥,顺序,等效密钥。
内部SDK/库:公共功能/例外/性能合同。
2)CDC模型: 角色和工件
消费者发布期望合同(大致查询/回答,类型匹配器,不变量)。
供应商对其服务/适配器/手柄进行合同验证。
合同经纪人(Pact Broker/Backstage/文物-repo)存储版本,标签("prod","staging","canary")和兼容性矩阵"consumer@v → provider@v"。
发布策略:如果违反任何"prod相关"合同,则禁止派遣提供商。
3)在合同中记录的内容(HTTP示例)
最低限度:- 方法/路径/参数/标题(包括auth,等效密钥)。
- 主体和类型对决器(类型/格式/regexp/范围)。
- 错误代码和结构;稳定的"error_code"。
- 语义不变性:排序,唯一性,单调性"created_at"。
- 非功能预期(可选):p95、尺寸限制、限额标题。
json
{
"interaction": "GET /v1/users/{id}",
"request": { "method": "GET", "path": "/v1/users/123", "headers": {"Accept":"application/json"} },
"matchers": {
"response.body.id": "type:number",
"response.body.email": "regex:^.+@.+\\..+$",
"response.body.created_at": "format:rfc3339"
},
"response": {
"status": 200,
"headers": {"Content-Type":"application/json"},
"body": {"id": 123, "email": "alice@example.com", "created_at": "2025-10-31T12:00:00Z"}
},
"error_cases": [
{
"name":"not_found",
"request":{"path":"/v1/users/9999"},
"response":{"status":404, "body":{"error_code":"USER_NOT_FOUND"}}
}
]
}
4)事件合同(事件驱动)
事件方案:"类型","版本","id","occurred_at_utc","生产者","主题","付费"。
不变性:"id"的不变性和"(类型,id)"的幂等性,派系键内的顺序,"序列"的单调性。
Schema Registry:存储演变和兼容性规则(backward/forward/full)。
Consumer合同测试:回应"黄金"事件和阴性阶段(未知字段,无效)。
json
{
"type":"record","name":"UserRegistered","namespace":"events.v1",
"fields":[
{"name":"id","type":"string"},
{"name":"occurred_at_utc","type":{"type":"long","logicalType":"timestamp-millis"}},
{"name":"email","type":"string"},
{"name":"marketing_opt_in","type":["null","boolean"],"default":null}
]
}
5)进化与互操作性
合同版本:语义'MAJOR。MINOR.PATCH'(MAJOR-打破)。
REST的规则:- 不要打破:不要删除字段,不要更改类型/值'error_code'。
- 添加可选的默认字段;新的endpoint而不是"魔术"。
- 消除:声明、并行支持、按指标删除。
- GraphQL:字段仅添加,非空通过相位输入;撤消指令。
- gRPC/Proto:不要重新使用字段编号;只添加新的选项。
- 活动:"vN"电路;消费者必须忽略未知字段(懒惰性)。
6)否定和不变检查
Negative:不正确的类型、禁止的值、冲突参数、超过限制。
Invariants:响应排序,"id"的唯一性,"next_cursor"的正确性,重复时等效响应的稳定性。
时间方面的合同:"created_at" RFC3339/UTC,正确的本地日投影不是运输合同的一部分-被赋予业务不变性。
7)生成和本地开发
从合同中产生了供货商来开发消费者。
对于事件-模式下的"有效/边界"消息生成器。
Stabs标记合同版本和组装日期;禁止发布到prod。
8)嵌入CI/CD(参考管道)
1.Consumer CI:
Lint/组装 →合同生成→ unit/合同测试 →在合同经纪人中发布(标签: 'consumer@1。7.0`).
2.Provider CI:
在当地/容器中提升服务→有关合同("prod"/"staging")→验证→向经纪人发布状态。
3.Release Gate:
如果合同尚未完成,则拒绝提供商。
4.Nightly Matrix:
兼容性矩阵"消费者版本×提供者版本";报告和警报。
9)跨域实践示例
9.1 REST:游标分区(合同不变量)
答案包含"items[]","next_cursor"(不可用),"limit"和"total"(可选)。
不变量:"len(items)≤ limit",使用相同的"cursor" →等效集重新调用。
如果同时设置"cursor"和"page",则出错。
9.2 POST等效性
合同要求标题"Idempotency-Key"。
不变性:使用相同键的重复查询返回相同的"id"/状态。
9.3事件:秩序保证
合同中的分期付款密钥为'partition_key=user_id'。
不变性:"序列"在键内单调增加;消费者必须处理重复。
10)合同中的安全和隐私
在示例中不包括PDn/秘密-仅合成。
记录强制性安全标题:"授权","X-Signature","Replay-Prevention"。
对于webhooks,是"2xx"/retraes的签名和响应合同。
在合同测试日志中-掩盖敏感字段。
11)工具
Pact/Pactflow/Pact Broker-HTTP/消息合同,兼容性矩阵。
OpenAPI/AsyncAPI-规范+测试生成器(Dredd,Schemathesis)。
空手道/REST保证是REST合同的脚本验证。
Protobuf/gRPC-"buf","protolint",兼容性测试;流中的Avro/JSON/Proto的计划注册。
GraphQL(graphql compat),快照电路测试的匹配测试。
12)提供商验证伪代码(简化)
python def verify_contract(provider, contract):
for case in contract["cases"]:
req = build_request(case["request"])
res = provider.handle(req) # локально/контейнер assert match_status(res.status, case["response"]["status"])
assert match_headers(res.headers, case["response"].get("headers", {}))
assert match_body(res.body, case["matchers"], allow_extra_fields=True)
for neg in contract.get("error_cases", []):
res = provider.handle(build_request(neg["request"]))
assert res.status == neg["response"]["status"]
assert res.json.get("error_code") == neg["response"]["body"]["error_code"]
13)反模式
"Postman截图是合同":没有版本/标准对决/自动验证。
超针:合同记录精确值而不是类型/模式→虚假下降。
一个用于不同区域/通道的一般合同:忽略可变性(标志,地理规则)。
没有经纪人/矩阵的合同:无法理解哪些版本是兼容的。
以e2e代替合同:缓慢,昂贵,不稳定。
没有负数/不变量:仅测试"绿色轨道"。
14)可观察性和操作
将状态导出到broker+dashboard"健康合同"。
Alerts:提供商对"prod"合同的新跌幅,事件中"未知领域"的增长。
跟踪:"contract_id"、"version"、"decision_id"在验证日志中。
15)撤消程序
1.添加字段/端口(不中断)。
2.在规范中将旧标记为"deprecated",宣布时间表。
3.通过记录/经纪人跟踪消费者;迁移海德。
4.在stadge中启用"shady" deny (dry-run),然后启用enforce。
5.在零使用比例后删除并确认兼容性。
16)建筑师支票清单
1.确定了消费者及其所有者?合同是否被验证?
2.是否存在与环境标签兼容的经纪人和矩阵?
3.合同中是否包括负值和不变量(特质,游标,排序)?
4.是否为事件配置了Schema Registry和兼容性模式?
5.在违反软件合同时,pipline是否会阻止提供商的发布?
6.描述了降解过程和进化策略?
7.从合同中生成稳定器,有本地事件发生器吗?
8.是否记录了PD掩蔽和强制性安全标题?
9.合同指标/Alerta是否已连接,是否有漂移报告?
10.双方(消费者和提供者)的CI审查合同?
二.结论
合同测试将有关相互作用的"真相"带入可转换的工件,并使集成可预测。CDC、合同经纪人和计划演变学科正在用一个可管理的过程取代"突破惊喜":快速验证、清晰的不变性和透明版本兼容性。这降低了e2e的成本,加快了发行速度,并提高了整个平台的质量。