Contract Testing Knowledge Share

契约开发学习成果分享

01 / 14

从“联调拉扯”到“契约协同”

这套分享聚焦契约精神:谁消费谁定义、最小必要字段、先保护协作再讨论实现。支持上下全屏切换,适合培训演示。

Contract / Pact CDCT Broker + CI Q&A 点击看答案
  • 目标:降低跨团队沟通损耗,减少“今天能跑、明天炸掉”的发布事故。
  • 路径:概念澄清 -> REST 实战 -> GraphQL 实战 -> 平台化治理。
  • 输出:可复用的讲义结构、代码示例、流程图与演练题。
Consumer Provider Pact Contract 测试的是接口约定,而不是业务逻辑正确性

什么是契约测试:先确认“说话格式”

契约测试不判断系统功能是否完整,它只验证 Consumer 和 Provider 是否仍然“听得懂彼此”。

🍔 外卖类比

你点单就是 Consumer,请求格式就是“点单协议”;汉堡店是 Provider,必须按协议返回。若字段名从 status 改成 order_status,Provider 验证会立刻见红。

拆分验证两半

消费者先在 Mock Provider 上定义期望并产出契约 JSON;提供者再拿这份契约跑真实接口验证,实现“离线协同、在线拦截”。

Step 1(Consumer):定义 interaction,生成 pact 文件。
Step 2(Provider):拉取 pact,执行 provider verification。
Step 3(Pipeline):不兼容直接阻断发布,避免生产破窗。

单元 / E2E / 契约:职责边界表

契约测试关注“协议稳定”,不替代单测,也不替代端到端。

测试类型 核心回答 优点 典型成本 最适用场景
单元测试 这段业务逻辑本身是否正确? 快、定位精准、可覆盖边界值 无法发现跨服务协议问题 金额计算、规则引擎、校验逻辑
E2E 测试 真实系统串联后是否可用? 最贴近真实用户链路 慢、脆弱、环境依赖重 下单、支付、退款等关键路径冒烟
契约测试 两边 API 约定是否兼容? 联调前就可发现破坏性变更 不覆盖复杂业务正确性 前端-BFF、BFF-微服务接口协作

CDCT 核心理念:前端是“甲方”

CDCT(Consumer-Driven Contract Testing)强调:消费者只声明“真正需要的字段”,推动提供者实现最小可用接口。

消费者驱动

不是 Provider 先拍脑袋写全量字段,而是 Consumer 先定义最小必要数据,减少过度耦合。

Postel 法则

发送严格、接收宽容。请求格式约束清晰,响应解析不要依赖无关字段。

BFF 双角色

BFF 对上游前端是 Provider,对下游后端是 Consumer。每层都要有自己独立契约。

前端(上游 Consumer) --(契约A)--> BFF(Provider) BFF(下游 Consumer) ----(契约B)--> 后端微服务(Provider) 结论:契约是分层管理,不是“一份万能大合同”。

避坑指南:三个思维转变

这三条是契约实践成败分水岭,建议在团队代码评审中设为硬性检查项。

  • 反模式一:把契约测试当集成测试。契约只管结构和类型,不测复杂业务规则。
  • 反模式二:Consumer 和 Provider 各玩各的。契约必须围绕消费者真实使用路径协作演进。
  • 反模式三:把业务边界值塞进契约。例如“负数金额触发哪条业务错误”应放到 Provider 单测。
  • 关键动作:全部使用 Matchers 关注 Shape,不要硬编码具体内容值。

例子 1:职责边界

错误:在 Pact 里断言“余额不足”文案与优惠规则。

正确:只断言 400 响应包含 errorCodemessage 字段;余额规则放 Provider 单测。

例子 2:避免硬编码

错误:固定断言 {"id":12345,"name":"Tom"}

正确:使用 MatchersV3.integer()MatchersV3.string(),关注类型与结构,不绑定具体值。

例子 3:交互要独立

错误:先调 /login 取 token,再链式调 /profile

正确:每个 Interaction 独立;通过 provider state(如已鉴权用户)准备上下文。

阶段小测:字段删改是否会拦截?

场景:前端已不再使用 user_avatar,BFF 删除该字段后会不会被契约阻断部署?

点击问题查看答案(可用于现场互动)

阶段二:BFF -> 后端 REST Pact(可直接讲解)

骨架流程:Mock Provider -> 定义 Interaction -> 执行真实客户端请求 -> 生成 Pact JSON。

import { PactV3, MatchersV3 } from '@pact-foundation/pact';

const provider = new PactV3({ consumer: 'BFFService', provider: 'BackendUserService' });

provider
  .uponReceiving('a request to get user profile')
  .withRequest({ method: 'GET', path: '/users/10' })
  .willRespondWith({
    status: 200,
    body: {
      id: MatchersV3.integer(10),
      username: MatchersV3.string('SampleUser')
    }
  });

await provider.executeTest(async (mockserver) => {
  // 调用真实 API Client
});
  • 关键词:PactV3、MatchersV3、executeTest。
  • 实践结论:不要测业务边界值,只测接口交互结构。

阶段二案例:同事想测 -99999 ID,该不该?

这是最常见误区:把契约测试当“业务规则验证场”。

错误做法

在 Pact 里写死负数 ID,希望触发 USER_NOT_FOUND 之类业务错误,导致契约脆弱、维护负担激增。

正确做法

只保留一个 4xx 错误结构契约,例如 {"errorCode": string}。边界值交给 Provider 单元测试覆盖。

测试关注点 契约测试 Provider 单元测试
错误 JSON 是否有 errorCode ✅ 关注 可选
-99999 是否触发 USER_NOT_FOUND ❌ 不关注 ✅ 必须覆盖

阶段三:前端 -> BFF GraphQL Pact

GraphQL 痛点:统一入口 /graphql,请求体是 Query 字符串,响应字段强依赖前端选择集。

import { PactV3, GraphQLInteraction, MatchersV3 } from '@pact-foundation/pact';

const graphqlCase = new GraphQLInteraction()
  .withQuery(queryUser)
  .withVariables({ id: '123' })
  .withOperation('GetUserProfile')
  .willRespondWith({
    status: 200,
    body: {
      data: {
        user: {
          id: MatchersV3.string('123')
        }
      }
    }
  });
  • 关键字必须理解:GraphQLInteraction、withQuery、withVariables。
  • BFF 双身份:向上做 Provider、向下做 Consumer,各自契约独立演进。
  • 治理建议:每个前端页面只声明自身真正消费字段,拒绝“预留一堆未来字段”。

阶段三思考:BFF 做 Provider 验证时怎么隔离下游?

当我们验证“前端 <-> BFF”契约时,BFF 对后端请求必须 mock,避免跨层耦合污染测试信号。

Frontend (上游 Consumer) | | contract verify v BFF Resolver (Provider under test) ----mock----> Backend API 重点:这里验证的是 BFF 的 Schema 映射,不是后端微服务可用性。
  • 上游 Consumer 契约失败,说明 BFF 对前端承诺未满足。
  • 下游 Consumer 契约失败,说明 BFF 调用后端的约定失真。
  • 术语提醒:BFF 对前端是 Provider,但对下游服务是上游 Consumer。

阶段四:Broker + CI/CD 交通枢纽

Broker 负责版本矩阵与可视化依赖,流水线负责“未兼容就不准上线”。

# Consumer 发布契约
pact-broker publish ./pacts --consumer-app-version=$GIT_SHA --branch=main

# Provider 拉取并验证
Verifier --provider-base-url=http://localhost:8080 --broker-url=$PACT_BROKER

# 发布闸门
pact-broker can-i-deploy --pacticipant web-bff --version $GIT_SHA --to-environment production
1. 发布契约:Consumer 将契约与版本绑定上传。
2. 自动触发验证:Provider pipeline 拉契约执行验证并回写结果。
3. 发布卡点:can-i-deploy 失败即阻断,避免不兼容版本上生产。

契约毕业场景题:大促前删字段

场景:后端删掉 last_login_ip,前端契约仍预期该字段。

点击问题查看答案

课堂演练(10 分钟)

请小组把“优惠券查询链路”拆成两份契约:前端-BFF 与 BFF-优惠券服务,并写出各自的验证和发布卡点。

补充篇:Provider 验证原理 + Code Review 要点

让团队不仅会“写 Pact”,还能在审查中识别坏味道。

Provider 验证 5 步

  • 从 Broker 拉取契约 JSON。
  • 按 state 准备测试上下文。
  • 发送契约请求到真实 Controller。
  • 按 Matcher 规则比对响应。
  • 回传验证结果到 Broker。

Code Review 红线

  • 使用 term() 时 generate 示例必须匹配正则。
  • 测试对象应是项目真实 API Client,而不是临时 axios 片段。
  • 优先 PactV3,避免旧 API 生命周期写法带来的接入复杂度。

Q&A 进阶:前端悄悄加字段会怎样?

这个问题最能体现契约“保护生产”的价值:不兼容需求不会被悄悄上线。

收尾:把契约精神固化为团队机制

建议将本页作为 onboarding 标准材料,并与模板仓库、CI 脚本、代码评审清单联动。

流程约束

每个跨服务接口必须有 Consumer 契约与 Provider 验证记录,禁止“口头字段约定”。

工程约束

CI 必须含 can-i-deploy;上线前必须具备可追溯 Broker 版本矩阵。

文化约束

坚持“只断言真实需求字段”,通过契约减少组织沟通摩擦,提升跨团队协作效率。