AI 参与说明(Agent:Codex):本文由 Codex 基于 Pact、Eiffel、Cucumber、Promptfoo、Apple 与 Android 官方资料协助整理。Contract Testing 术语与工具能力以来源为准;跨端 Specification 的组织方式是本文的工程建议,不代表统一行业标准。
资料与修订记录
本次修订运行记录(2026-09-07,Agent:Codex):模型
gpt-6-astra;reasoning effort:medium;执行入口:Codex Desktop;提供方:openai;运行记录中的 CLI 版本:0.153.4(不代表桌面 App 版本)。参数取自本次任务运行记录,仅归属于本次修订与校验,原有撰写参与说明保留。
英文表述修订(2026-09-07,Agent:Codex):通过 Context7 与 Pact 原始文档核对 Contract 定义及验证范围,并用 Mermaid 重构双端验证流程。此次运行记录:模型
gpt-6-astra,reasoning effortultra,执行入口 Codex Desktop,提供方openai,CLI 版本0.153.4(不代表桌面 App 版本);上方medium记录仍归属于此前修订与校验。
术语一致性修订(2026-09-08,Agent:Codex):统一全文、图表与示例中的概念名称,明确 Contract 与 Specification 的边界,并同步 Python 示例的数据字段、变量与预期输出。运行记录:模型
gpt-6-astra,reasoning effortultra,执行入口 Codex Desktop,提供方openai,CLI 版本0.153.4(不代表桌面 App 版本)。
阅读导引修订(2026-09-08,Agent:Codex):把核心术语的中文名称与简要解释置于正文之前,方便先理解名称,再阅读英文定义与论述。运行记录:模型
gpt-6-astra,reasoning effortultra,执行入口 Codex Desktop,提供方openai,CLI 版本0.153.4(不代表桌面 App 版本)。
整理日期:2026-09-07。本文面向 Web、iOS、Android、服务端及共享业务模块。Python 最小示例仅验证共享 Test Vector 与规则执行流程;未运行真实 iOS / Android 工程、Pact 双端集成或 LLM-as-a-judge,不能据此认定某个客户端已经通过验收。
阅读前先看这几个词
先用这张表认识文章的核心概念。中文名称用于词义对照,简要解释帮助理解;后文继续使用左栏的英文名称,具体条件和例子会在对应小节展开。
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| Contract Testing | 契约测试 | 检查两个系统交换的数据是否符合双方约定 |
| Contract | 契约 | 接口交互中必须遵守的约定,例如字段名称、类型和错误码 |
| Specification | 规格说明 | 功能应满足的业务要求,例如离线时必须保存草稿 |
| Consumer | 消费者 | 使用接口的一方,例如查询订单的手机应用 |
| Provider | 提供者 | 提供接口的一方,例如返回订单数据的服务 |
| Consumer-driven Contract Testing | 消费者驱动的契约测试 | 从接口使用方的实际需要出发形成约定,再分别检查两端实现 |
| Conformance Testing | 符合性测试 | 检查实现是否满足已经写明的要求 |
| Consumer Test | 消费者测试 | 检查接口使用方如何发送数据、处理返回结果的测试 |
| Provider Verification | 提供者验证 | 检查接口提供方能否按照约定处理交互并返回结果的过程 |
| Test Case | 测试用例 | 要执行和检查的具体场景,例如未登录时提交草稿 |
| Test Vector | 测试向量 | 一组输入与预期输出的数据,例如未登录、离线时应返回什么 |
| Test Adapter | 测试适配器 | 把共享测试数据接到各端真实业务代码上的连接代码 |
| Test Oracle | 测试预言机 | 判断实际结果是否符合预期的依据或机制 |
| LLM-as-a-judge | 大语言模型作为裁判 | 让语言模型根据给定标准评价结果的方法 |
| Rubric | 评分量表 | 明确评价哪些方面、怎样评分的标准 |
核心观点
Contract Testing checks whether messages exchanged at an integration boundary satisfy the agreement recorded in a Contract. Pact:What is contract testing?
这里的重点是系统交互边界的兼容性。多端共同遵守的业务要求统一称为 Specification;用于记录 Consumer 与 Provider 交互约定的产物称为 Contract。它们的验证目标不同,不能把针对 Specification 的所有测试都重新命名为 Contract Testing。
Conformance Testing 检查实现是否满足 Specification。它可能与 Contract Testing 在接口验证上相交,但不能把跨端共享的 Specification 自动当成 Pact 生成的 Contract。
自然语言可以表达 Specification。工具可以把自然语言编写的 Specification 转换成测试、绑定到可执行步骤,或用作 LLM-as-a-judge 的 Rubric;这些方式是否属于 Contract Testing,取决于它们实际验证的对象。
先分清三种验证关系
| 关系 | 主要问题 | 适合的验证方式 |
|---|---|---|
| iOS / Android / Web 与服务端 | Request、Response、错误码是否与 Consumer 兼容 | Consumer-driven Contract Testing 或 Conformance Testing |
| iOS 与 Android 实现同一功能 | 同样条件下,业务决策、状态转换和 Side Effect 是否符合共同的 Specification | 共享 Test Vector、状态序列、各端 Conformance Testing |
| 一个函数或模块的调用者与实现 | Precondition、Postcondition 和 Invariant 是否成立 | Design by Contract、Assertion、Unit Testing、Property-based Testing |
如果两个客户端并不直接交换消息,它们通常不是彼此的 Consumer 与 Provider。它们是同一份 Specification 的两个实现。若它们通过文件、二维码或设备协议互通,则该交换边界本身也需要 Contract Testing。
Contract Testing:两边各自通过测试仍然可能不兼容
假设客户端读取订单接口返回的 totalCents,约定它是以分计的整数;后端重构时把它改成了 total 字符串。如果客户端只测试自己写的 Mock,后端只测试自己的新实现,两边执行 Unit Testing 时都可能得到通过结果,集成后却失败。Contract Testing 的价值在于检查边界上的约定是否仍成立,而不是只检查各自内部是否自洽。
Consumer-driven Contract Testing 用同一份 Contract 连接两端的验证:
flowchart TB
subgraph CT["Consumer Test"]
C["真实 API Client"] -->|"Request"| M["Pact Mock Provider"]
M -->|"Response"| C
end
M -->|"记录已覆盖的 Interaction"| P["Contract"]
P --> V["Provider Verification<br/>对真实 Provider 重放 Request<br/>比对 Response 与 Contract"]
V --> CI["CI 使用对应版本的验证结果"]
Provider Verification 前,需要准备对应的 Provider State。CI 根据待交付版本的验证结果判断已覆盖的 Interaction 是否兼容。
只有 Consumer Test 通过,不能证明 Provider 满足 Contract;只验证 Provider 符合自己写的 Schema,也不能证明现有 Consumer 可以使用它。对已发布接口 Specification 进行 Conformance Testing,也可能验证 Contract,但应交代它不构成 Consumer-driven Contract Testing 的完整证据。Pact Consumer Tests How Pact works
Contract 不必把整个 JSON 精确冻结:需要约束 Consumer 依赖的字段、类型、状态码、Header 和明确的取值规则,同时在兼容性策略允许时容忍新增无关字段。totalCents 仍是整数却从“分”改为“元”,仅检查 Response 的字段和类型就可能漏过;需要能识别单位错误的样例或针对业务含义的 Assertion。
Pact uses Contract Testing to verify message agreement between a Consumer and a Provider. Side Effects, such as database writes, require separate Functional Testing. Contract tests are not functional tests
因此,金额计算本身是否正确、订单是否真正落库,要在 Provider 的 Functional Testing 中验证;不能只看到 Response 字段符合 Contract 就认定业务操作成功。
另一个相关但不同的概念是 Design by Contract:用 Precondition、Postcondition 和 Invariant 表达调用者与被调用者的责任。例如输入数量必须为正、成功后库存按约定减少、库存不得为负。这是一种设计方法,可以通过 Assertion 和测试落实;它不等同于 Pact 的 Consumer / Provider 验证流程。Eiffel:Design by Contract and Assertions
iOS 与 Android:共享 Specification,各自验证真实实现
假设两端都有“提交离线草稿”功能。可以共同约定:离线时保存为待同步状态;恢复网络后提交;请求超时后使用同一业务操作标识重试;收到成功确认后再标记已同步。两端不必具有相同按钮位置或相同导航动画,但持久化结果与重试语义应符合相同业务要求。
建议把共同要求分成三层,并为平台差异建立显式条目:
| 层次 | 共享内容 | 每端验证什么 |
|---|---|---|
| 纯业务规则 | 输入、预期输出、错误优先级、单位、边界值 | 真实业务函数,而不是测试里重写的函数 |
| 有状态流程 | 初始状态、事件序列、依赖响应、最终状态、允许的 Side Effect | 状态机、持久化、重试与恢复行为 |
| 平台体验 | 用户目标与可观察业务结果 | 原生 UI、生命周期、权限交互与平台集成 |
例如,可以共同规定“提交请求发出前取消编辑,不产生提交操作”,并在各端检查对应的取消入口。请求已经发出时能否取消、如何处理成功响应,需要另列规则;返回上一页本身不能证明请求或服务端写入已取消。UI 截图基线通常应按平台管理,不能把两端像素相等当成功能一致的必要条件。权限能力、后台执行和版本差异也应明确范围;不适用的规则需要记录理由,不能悄悄跳过后计入通过。
用共享 Test Case 数据落地
一个低成本起点是:Specification 用 Markdown 表达,共享的 Test Case 数据用 JSON 保存,各端 Test Adapter 读取它们,并调用各自生产代码。下面的 JSON 中,每条记录是一个 Test Case:id 标识它,input 与 expected 组成该 Test Case 使用的 Test Vector。需要固定版本的是整份 Test Case 数据文件。三者职责不同:
- Specification 说明为什么这样规定,以及规则优先级。
- Test Case 通过具体的 Test Vector 检查一个场景,包括边界与反例。
- Test Adapter 只做数据转换、依赖注入、调用和结果提取,不能重新实现业务判断。
期望值应来自经过审阅的 Specification,不从当前任一客户端的实际输出自动生成。仅比较 iOS 与 Android 输出是否相等属于 Differential Testing 思路:它能发现分歧,却不能排除两端一起出错。因此最好分别比较“iOS 与 Specification”“Android 与 Specification”,再把跨端差异作为辅助信号。
最小可运行示例:共同的草稿提交规则
以下示例使用 Python 3.9+ 标准库,无第三方依赖。这里的 specificationVersion 是本示例自定义的数据字段,用于标记业务要求的版本;它不是 Pact 文件格式。示例只演示 Specification 与实现的连接方式,不替代 iOS / Android 的平台测试。为了独立运行,规则函数与校验器放在同一文件;接入实际项目时,应把规则函数替换为对生产模块的调用,不能保留一份测试专用的业务实现。业务规则为:未登录优先返回 sign_in_required;已登录且离线返回 queued;已登录且在线返回 send。输入限定为两个布尔值,非法输入策略不在此示例范围内。
将下面内容保存为 submission-cases.json:
{
"specificationVersion": "1.0.0",
"cases": [
{"id": "SUBMIT-01", "input": {"signedIn": false, "online": false}, "expected": "sign_in_required"},
{"id": "SUBMIT-02", "input": {"signedIn": false, "online": true}, "expected": "sign_in_required"},
{"id": "SUBMIT-03", "input": {"signedIn": true, "online": false}, "expected": "queued"},
{"id": "SUBMIT-04", "input": {"signedIn": true, "online": true}, "expected": "send"}
]
}
将下面内容保存为 verify_submission.py,与 JSON 文件放在同一目录:
import json
from pathlib import Path
def decide_submission(signed_in, online):
if not signed_in:
return "sign_in_required"
if not online:
return "queued"
return "send"
def verify(decide, cases):
failures = []
for case in cases:
data = case["input"]
actual = decide(data["signedIn"], data["online"])
if actual != case["expected"]:
failures.append(f'{case["id"]}: {actual} != {case["expected"]}')
if failures:
raise AssertionError("\n".join(failures))
if __name__ == "__main__":
source = Path(__file__).with_name("submission-cases.json")
specification = json.loads(source.read_text(encoding="utf-8"))
if specification["specificationVersion"] != "1.0.0":
raise ValueError("Unsupported specification version")
cases = specification["cases"]
if not cases:
raise ValueError("No test cases discovered")
verify(decide_submission, cases)
print(f'{len(cases)} cases passed; specification {specification["specificationVersion"]}')
运行 python3 verify_submission.py,预期输出 4 cases passed; specification 1.0.0。若故意把离线判断放到登录判断之前,SUBMIT-01 应失败:这证明 Test Case 可以发现这一种规则优先级错误,不能据此推断对其他缺陷也有同等检出能力。四个 Test Vector 覆盖了两个布尔输入的全部组合,但不覆盖持久化、并发、生命周期或网络 Side Effect。
实际接入时,iOS 测试读取同一 JSON,调用 Swift 业务模块;Android 测试读取同一 JSON,调用 Kotlin 业务模块。两端都输出 Test Case ID、Specification 版本、实现提交与实际结果。Swift Testing 支持以参数集合运行同一测试,可作为 iOS 纯业务 Test Case 的承载方式;Android 则应按是否依赖设备能力选择 Local Test 或 Instrumented Test。Apple Parameterized Testing Android Testing Fundamentals
各端 Test Adapter 的职责与失败报告
共享的是 Specification 与 Test Case 数据,不要求共享测试框架。接入每个客户端时,按以下顺序建立证据:
- 将固定版本的 Test Case 数据文件纳入测试资源,并核对必需 Test Case ID、重复 ID 与空集合,防止漏加载被误报为通过。
- 把输入转换为生产模块接受的类型,注入可控的时钟、网络与存储依赖,保持每个 Test Case 的初始状态独立。
- 调用真实业务入口,提取返回值、状态或 Side Effect;只按 Specification 允许的规则归一化结果。例如可以统一时间的序列化格式,但不能抹掉业务相关的时区差异。
- 逐项与共享期望值比较,在失败报告中保留 Test Case ID、期望值、实际值、Specification 版本和实现版本。
若同一逻辑已由共享模块实现,模块测试可以减少重复,但各平台的绑定、序列化、存储与生命周期集成仍需验证。共享实现只能减少部分行为漂移,不能代替平台集成证据。
从纯函数扩展到状态与 Side Effect
上面的 queued 只是决策结果,不能证明草稿已经落盘。完整功能还需要固定初始数据与事件序列,并观察持久化和真实依赖边界:
| Test Case ID | 初始状态与事件 | 应验证的结果 |
|---|---|---|
| SYNC-01 | 已登录、离线,提交草稿后重新打开应用 | 草稿仍存在且处于待同步状态 |
| SYNC-02 | 同步请求在服务端成功,客户端未收到响应后重试 | 重用业务操作标识;服务端只有一条对应业务记录 |
| SYNC-03 | 待同步时退出账号,再切换账号 | 按明确的账号隔离策略处理,不把旧账号草稿上传到新账号 |
这是示意性 Specification,尚未绑定平台执行代码。应明确请求超时的故障注入点、时钟、网络恢复事件、存储状态与观察时限;用明确的完成信号或有上限的条件等待,避免依赖任意长的固定等待。
“只写入一次”需要覆盖服务端幂等逻辑和实际持久化,不能只检查客户端发了几次请求。检查客户端业务规则、执行 Contract Testing、执行 Integration Testing 和运行设备场景分别提供不同证据,不能互相替代。
移动端版本矩阵:不只测最新两个版本
移动端应根据实际支持策略列出仍在使用的客户端版本,以及它们依赖的 Specification 与 Contract 版本。下面只是示意,不是具体产品的兼容承诺:
| 实现 | Specification | Contract | 发布前要求 |
|---|---|---|---|
| 仍受支持的 iOS 旧版 | 1.x | API v1 的实际依赖 | 新服务端继续满足其依赖 |
| iOS 新版 | 2.x | API v1 或 v2 | 新行为符合 2.x,并覆盖升级与旧数据 |
| Android 当前版 | 1.x | API v1 的实际依赖 | 保留 1.x 行为;明确尚未支持的能力 |
共同的 Specification 不意味着所有客户端必须同时升级。某个新功能暂时只在一端提供时,应声明功能的适用版本、未支持时如何处理,以及退出支持的条件;不能把“尚未支持”写成无说明的通过。
CI 应固定 Test Case 数据文件的版本或摘要,验证必需 Test Case ID 已被发现,区分通过、失败、未运行和经批准的不适用。兼容性判断以仍受支持的版本组合为准;只让同一个 Agent 同时更新最新前后端并跑绿,会漏掉旧客户端。
自然语言测试方案能否称为 Contract Testing
自然语言描述的是表达方式,Contract Testing 描述的是验证目标。判断名称时,应分开看 Specification、Test Runner 和 Test Oracle(用于判断实际结果是否符合预期的依据或机制)。
| 方案 | 如何执行与判断 | 更准确的称呼 |
|---|---|---|
在 AGENTS.md 写“接口必须向后兼容” | 仅供 Agent 阅读,没有独立执行证据 | 项目规则或行为约束,还没有实际执行的测试 |
| 给 Agent 一段需求,让它生成并运行 Assertion | Agent 翻译需求,Test Runner 执行具体 Assertion | 自然语言驱动的测试生成;测试类型取决于 Assertion 对象 |
| Gherkin 场景绑定 Step Definition | Step Definition 执行动作与 Assertion | Executable Specification;Behavior-driven Development 用它连接讨论与实现,覆盖验收目标时也可用于 Acceptance Testing |
| 自然语言定义接口交互,再验证 Consumer / Provider | 检查 Consumer 与 Provider 对 Request / Response 的约定 | 可以是 Contract Testing,关键是实际验证接口边界 |
| 用自然语言 Rubric 让 LLM 判断结果 | 模型输出评分、理由或 Pass / Fail | LLM-as-a-judge |
因此,“用户点击保存后看到成功提示”即便以自然语言写成并自动执行,通常仍是针对 UI 的 Acceptance Testing;“订单接口在指定状态下返回整数 totalCents,且现有 Consumer 能解析”才具有 Contract Testing 的目标。Gherkin 并不决定测试处在哪一层,Step Definition 才连接实际被测对象。Cucumber Step Definitions
Promptfoo 的 llm-rubric 是后一类方案的具体例子:以自然语言给出 Rubric,由模型评估输出。这里自然语言并非没有 Test Oracle,而是采用了模型作为 Test Oracle。与 Deterministic Assertion 相比,它需要额外管理 Rubric、模型配置、输入证据和误判;即使输出一个布尔值,也不意味着判定已经确定化。Promptfoo LLM Rubric
使用 LLM-as-a-judge 检查行为描述,不能据此宣称已经完成 Contract Testing。精确字段、权限拒绝、数据条数和状态转换优先使用 Deterministic Assertion;“解释是否清晰”这类语义要求可用 LLM-as-a-judge,并用人工标注的正反例校准。验证代码功能时应提供实际运行结果、页面状态或持久化证据,不能只让 LLM 阅读 Coding Agent 的完成报告就作为判断依据。
Coding Agent 如何使用 Specification 与 Contract
这里需要区分两个对象:Agent 编写的软件可以用 Unit Testing、Contract Testing、Integration Testing 和 End-to-end Testing 验证;Agent 本身的行动与回答则可能需要工具调用轨迹 Assertion 与 Agent Evaluation。让 Codex 写普通应用,不意味着应用的测试也必须使用 LLM-as-a-judge。
下面是借助 Coding Agent 开发软件的工程建议,不是新的行业测试分类:
- 把意图变成先于实现的 Acceptance Criteria。给关键要求分配稳定 ID,写清前置状态、动作、可观察结果和错误场景,再让 Agent 映射到合适的测试。无需机械地把每个小改动都变成完整 Test-driven Development 流程。
- 在接口边界约束生成的实现。客户端和服务端分开实现时,共享可验证的交互约定,并分别验证真实 Consumer 与 Provider。若旧版客户端仍在使用,不能只验证同一次改动后的新客户端和新服务端;还应覆盖仍需支持的 Consumer 的 Contract。
- 让 Agent 寻找反例。要求它设计缺字段、错误类型、越权、超时或重试场景,并明确哪些应由 Contract Testing 覆盖,哪些应由 Integration Testing 覆盖。回归修复应证明新增测试在未修复版本上失败,修复后通过;必要时用人为注入的错误检查测试是否敏感。
- 保持 Acceptance Criteria 的独立性。可以让同一个 Coding Agent 实现功能并补充测试,但降低 Assertion 强度、跳过测试或更新预期值,需要单独审阅并说明原因。第二个 Agent 可以辅助审阅,但不构成独立正确性的保证。
- 在 CI 中保留可复查证据。记录被测提交、实际运行的测试和结果;关键兼容性检查应成为 Required Check。仅在 Prompt 中写“必须通过测试”并不能阻止不合格代码合入。
例如,可以给 Coding Agent 以下任务模板。其中的 Acceptance Criteria 用自然语言编写,整段 Prompt 并不是可以直接运行的测试文件:
Goal: implement order lookup on iOS and Android.
AC-API-01: For an existing order, the Consumer can parse a string id and an
integer totalCents measured in cents. Use the same Contract for the
Consumer Test and Provider Verification.
AC-API-02: For a missing order, return HTTP 404 and the agreed error code.
AC-UI-01: On load failure, show the error and a retry action; do not show success.
AC-DATA-01: Unauthorized users cannot read order contents. Verify this with
Integration Testing at the real authorization boundary.
AC-CROSS-01: Both implementations follow the same Specification for loading,
success, empty results, and failure. Call real business modules and verify
each platform's UI separately.
Before implementation, map each Acceptance Criterion ID to a testing method
and observable evidence. Pin the shared Test Case data version and report
which Test Case IDs actually ran on each platform.
Preserve compatibility with supported older Consumers and their Contracts.
Report conflicts between implementation and Specification; do not hide them
by weakening Assertions or changing expected results.
Report executed commands, results, and scenarios that remain uncovered.
在需要精确重现“未授权时没有发生写入”或“重试没有重复创建订单”的场景中,应直接检查状态与 Side Effect;这类证据不能由 Response 的字段和类型检查,或模型的自然语言判断替代。
选择起点与维护成本
如果主要问题是 iOS / Android 业务规则漂移,先从一项高价值功能开始,共享少量覆盖边界的 Test Case 数据,让各端现有测试调用真实代码。无需先搭建庞大的跨平台框架。
当客户端与服务端独立发布、Mock 经常与真实响应不一致时,再引入 Consumer-driven Contract Testing。只有服务端对 Specification 的校验时,也应如实报告其覆盖范围,不能把它当成已验证所有客户端。
对于权限拒绝、金额、数据隔离和持久化次数等明确结果,优先采用 Deterministic Assertion。LLM-as-a-judge 适合语义目标,但要固定 Rubric 与模型配置,用人工标注的正反例校准,并保留实际运行证据。Coding Agent 的完成报告本身不是被测系统的行为证据。
关联阅读
-
Agent 开发中的测试方法与 Test Oracle:了解 Property-based Testing、Mutation Testing、Model-based Testing 与 Fault Injection 如何补充 Contract Testing。
-
前端 UI 的测试层次与验收证据:Web Component、Browser Flow、视觉与可访问性验证。
-
技术文档术语与 Mermaid 改写规范:统一概念名称,同时保留中文解释。