AI 参与说明(Agent:Codex):本文由 Codex 基于 Pact、Eiffel、Cucumber、Promptfoo、Apple 与 Android 官方资料协助整理。接口测试术语与工具能力以来源为准;“跨端功能契约”及工作流是本文的工程组织方式,不代表统一行业标准。
本次修订运行记录(2026-09-07,Agent:Codex):模型
gpt-6-astra;reasoning effort:medium;执行入口:Codex Desktop;提供方:openai;运行记录中的 CLI 版本:0.153.4(不代表桌面 App 版本)。参数取自本次任务运行记录,仅归属于本次修订与校验,原有撰写参与说明保留。
整理日期:2026-09-07。本文面向 Web、iOS、Android、服务端及共享业务模块。Python 最小示例仅验证共享测试向量与规则执行流程;未运行真实 iOS / Android 工程、Pact 双端集成或 LLM Judge,不能据此认定某个客户端已经通过验收。
核心观点#
契约是对输入、输出、行为或边界责任的明确约定;测试检查实际实现是否满足这些约定。通常所说的 Contract Testing 重点是系统交互边界的兼容性,但团队也可以用“功能契约”组织多端共同遵守的业务规则。必须说清验证对象、执行范围和判定机制,不能把所有测试都重新命名为 Contract Test。
自然语言是规格的表达方式。工具可以把自然语言规格转换成测试、绑定到可执行步骤,或用作 LLM Judge 的评分规则;这些方式是否属于接口契约测试,取决于它们实际验证的对象。
先分清三种契约关系#
| 关系 | 主要问题 | 适合的验证方式 |
|---|---|---|
| iOS / Android / Web 与服务端 | 请求、响应、错误码是否与使用方兼容 | Consumer-driven Contract Testing 或 Specification Conformance |
| iOS 与 Android 实现同一功能 | 同样条件下,业务决策、状态转换和副作用是否符合共同规格 | 共享测试向量、状态序列、各端 Conformance Test |
| 一个函数或模块的调用者与实现 | 前置条件、后置条件和不变量是否成立 | Design by Contract、断言、Unit / Property-based Test |
如果两个客户端并不直接交换消息,它们通常不是彼此的 Consumer 与 Provider。它们是同一份业务规格的两个实现。若它们通过文件、二维码或设备协议互通,则该交换边界本身也需要接口或协议契约验证。
接口契约:两边各自通过测试仍然可能不兼容#
假设客户端读取订单接口返回的 totalCents,约定它是以分计的整数;后端重构时把它改成了 total 字符串。如果客户端只测试自己写的 Mock,后端只测试自己的新实现,两边的 Unit Test 都可能通过,集成后却失败。Contract Test 的价值在于检查边界上的约定是否仍成立,而不是只检查各自内部是否自洽。
Consumer-driven Contract Testing 的典型流程是:
- Consumer Test 调用真实的客户端 API Client,让它与契约工具提供的 Mock Provider 交互。
- 工具从这些 Interaction 生成 Contract,记录 Consumer 实际依赖的 Request / Response 约束。
- Provider Verification 在准备好对应状态的真实 Provider 实现上验证同一份 Contract。
- CI 使用与待交付版本相关的验证结果,判断这组 Consumer / Provider 是否满足已覆盖的交互约定。
只有客户端 Mock Test 通过,不能证明后端履约;只验证后端符合自己写的 Schema,也不能证明现有 Consumer 可以使用它。Provider 对已发布 Specification 的符合性检查仍可以称为 Contract Testing,但应交代它不是 Consumer-driven 的完整证据。Pact Consumer Tests How Pact works
契约不必把整个 JSON 精确冻结:需要约束 Consumer 依赖的字段、类型、状态码、Header 和明确的取值规则,同时在兼容性策略允许时容忍新增无关字段。totalCents 仍是整数却从“分”改为“元”,单纯 Shape Check 就可能漏过;需要能识别单位错误的样例或语义断言。金额计算本身是否正确、订单是否真正写入数据库,则还需要 Functional / Integration Test。Pact 官方明确区分 Message Agreement 与 Provider Side Effect 的验证。Contract tests are not functional tests
另一个相关但不同的概念是 Design by Contract:用 Precondition、Postcondition 和 Invariant 表达调用者与被调用者的责任。例如输入数量必须为正、成功后库存按约定减少、库存不得为负。这是一种设计方法,可以通过断言和测试落实;它不等同于 Pact 的 Consumer / Provider 验证流程。Eiffel:Design by Contract and Assertions
iOS 与 Android:共享功能规格,各自验证真实实现#
假设两端都有“提交离线草稿”功能。可以共同约定:离线时保存为待同步状态;恢复网络后提交;请求超时后使用同一业务操作标识重试;收到成功确认后再标记已同步。两端不必具有相同按钮位置或相同导航动画,但持久化结果与重试语义应符合相同业务要求。
建议把共同要求分成三层,并为平台差异建立显式条目:
| 层次 | 共享内容 | 每端验证什么 |
|---|---|---|
| 纯业务规则 | 输入、预期输出、错误优先级、单位、边界值 | 真实业务函数,而不是测试里重写的函数 |
| 有状态流程 | 初始状态、事件序列、依赖响应、最终状态、允许的副作用 | 状态机、持久化、重试与恢复行为 |
| 平台体验 | 用户目标与可观察业务结果 | 原生 UI、生命周期、权限交互与平台集成 |
例如,可以共同规定“提交请求发出前取消编辑,不产生提交操作”,并在各端检查对应的取消入口。请求已经发出时能否取消、如何处理成功响应,需要另列规则;返回上一页本身不能证明请求或服务端写入已取消。UI 截图基线通常应按平台管理,不能把两端像素相等当成功能一致的必要条件。权限能力、后台执行和版本差异也应明确范围;不适用的规则需要记录理由,不能悄悄跳过后计入通过。
用共享测试向量落地#
一个低成本起点是:业务规格用 Markdown 表达,同一批输入与期望值用 JSON 保存,各端测试适配层读取它们,并调用各自生产代码。三者职责不同:
- 规格说明为什么这样规定,以及规则优先级。
- 测试向量提供可以复现的例子,包括边界与反例。
- Adapter 只做数据转换、依赖注入、调用和结果提取,不能重新实现业务判断。
期望值应来自经过审阅的规格,不从当前任一客户端的实际输出自动生成。仅比较 iOS 与 Android 输出是否相等属于 Differential Testing 思路:它能发现分歧,却不能排除两端一起出错。因此最好分别比较“iOS 与规格”“Android 与规格”,再把跨端差异作为辅助信号。
最小可运行示例:共同的草稿提交规则#
以下示例使用 Python 3.9+ 标准库,无第三方依赖。它只演示规格与实现的连接方式,不是 iOS / Android 测试替身。为了独立运行,规则函数与校验器放在同一文件;接入实际项目时,应把规则函数替换为对生产模块的调用,不能保留一份测试专用的业务实现。业务规则为:未登录优先返回 sign_in_required;已登录且离线返回 queued;已登录且在线返回 send。输入限定为两个布尔值,非法输入策略不在此示例范围内。
将下面内容保存为 submission-cases.json:
{
"contractVersion": "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")
contract = json.loads(source.read_text(encoding="utf-8"))
if contract["contractVersion"] != "1.0.0":
raise ValueError("Unsupported contract version")
cases = contract["cases"]
if not cases:
raise ValueError("No test cases discovered")
verify(decide_submission, cases)
print(f'{len(cases)} cases passed; contract {contract["contractVersion"]}')运行 python3 verify_submission.py,预期输出 4 cases passed; contract 1.0.0。若故意把离线判断放到登录判断之前,SUBMIT-01 应失败:这证明用例可以发现这一种规则优先级错误,不能据此推断对其他缺陷也有同等检出能力。四个向量覆盖了两个布尔输入的全部组合,但不覆盖持久化、并发、生命周期或网络副作用。
实际接入时,iOS 测试读取同一 JSON,调用 Swift 业务模块;Android 测试读取同一 JSON,调用 Kotlin 业务模块。两端都输出 Case ID、契约版本、实现提交与实际结果。Swift Testing 支持以参数集合运行同一测试,可作为 iOS 纯业务用例的承载方式;Android 则应按是否依赖设备能力选择 Local Test 或 Instrumented Test。Apple Parameterized Testing Android Testing Fundamentals
各端 Adapter 的职责与失败报告#
共享的是规格与用例数据,不要求共享测试框架。接入每个客户端时,按以下顺序建立证据:
- 将固定版本的 JSON 用例纳入测试资源,并核对必需 Case ID、重复 ID 与空集合,防止漏加载被误报为通过。
- 把输入转换为生产模块接受的类型,注入可控的时钟、网络与存储依赖,保持每个用例的初始状态独立。
- 调用真实业务入口,提取返回值、状态或副作用;只按规格允许的规则归一化结果。例如可以统一时间的序列化格式,但不能抹掉业务相关的时区差异。
- 逐项与共享期望值比较,在失败报告中保留 Case ID、期望值、实际值、契约版本和实现版本。
若同一逻辑已由共享模块实现,模块测试可以减少重复,但各平台的绑定、序列化、存储与生命周期集成仍需验证。共享实现只能减少部分行为漂移,不能代替平台集成证据。
从纯函数扩展到状态与副作用#
上面的 queued 只是决策结果,不能证明草稿已经落盘。完整功能还需要固定初始数据与事件序列,并观察持久化和真实依赖边界:
| Case ID | 初始状态与事件 | 应验证的结果 |
|---|---|---|
| SYNC-01 | 已登录、离线,提交草稿后重新打开应用 | 草稿仍存在且处于待同步状态 |
| SYNC-02 | 同步请求在服务端成功,客户端未收到响应后重试 | 重用业务操作标识;服务端只有一条对应业务记录 |
| SYNC-03 | 待同步时退出账号,再切换账号 | 按明确的账号隔离策略处理,不把旧账号草稿上传到新账号 |
这是示意性场景规格,尚未绑定平台执行代码。应明确请求超时的故障注入点、时钟、网络恢复事件、存储状态与观察时限;用明确的完成信号或有上限的条件等待,避免依赖任意长的 Sleep。
“只写入一次”需要覆盖服务端幂等逻辑和实际持久化,不能只检查客户端发了几次请求。客户端规则测试、接口契约测试、集成测试和设备场景测试分别提供不同证据,不能互相替代。
移动端版本矩阵:不只测最新两个版本#
移动端应根据实际支持策略列出仍在使用的客户端版本与契约版本。下面只是示意,不是具体产品的兼容承诺:
| 实现 | 业务契约 | 接口契约 | 发布前要求 |
|---|---|---|---|
| 仍受支持的 iOS 旧版 | 1.x | API v1 的实际依赖 | 新服务端继续满足其依赖 |
| iOS 新版 | 2.x | API v1 或 v2 | 新行为符合 2.x,并覆盖升级与旧数据 |
| Android 当前版 | 1.x | API v1 的实际依赖 | 保留 1.x 行为;明确尚未支持的能力 |
共同规格不意味着所有客户端必须同时升级。某个新功能暂时只在一端提供时,应声明 Feature / Capability 的适用版本、Fallback 和退出支持条件;不能把“尚未支持”写成无说明的通过。
CI 应固定测试向量版本或摘要,验证必需 Case ID 已被发现,区分通过、失败、未运行和经批准的不适用。兼容性判断以仍受支持的版本组合为准;只让同一个 Agent 同时更新最新前后端并跑绿,会漏掉旧客户端。
自然语言测试方案能否称为契约测试#
自然语言描述的是表达方式,Contract Testing 描述的是验证目标。判断名称时,应分开看 Specification、执行器和 Test Oracle(判定实际结果是否满足预期的机制)。
| 方案 | 如何执行与判断 | 更准确的称呼 |
|---|---|---|
在 AGENTS.md 写“接口必须向后兼容” | 仅供 Agent 阅读,没有独立执行证据 | 项目规则或行为约束,还不是 Test |
| 给 Agent 一段需求,让它生成并运行断言 | Agent 翻译需求,Test Runner 执行具体断言 | 自然语言驱动的测试生成;测试类型取决于断言对象 |
| Gherkin 场景绑定 Step Definition | Step Definition 执行动作与断言 | Executable Specification / BDD 场景;覆盖验收目标时也是 Acceptance Test |
| 自然语言定义接口交互,再验证 Consumer / Provider | 检查双方对 Request / Response 的约定 | 可以是 Contract Test,关键是实际验证接口边界 |
| 用自然语言评分规则让 LLM 判断结果 | 模型输出评分、理由或 Pass / Fail | LLM-as-a-judge Evaluation / Semantic Assertion |
因此,“用户点击保存后看到成功提示”即便以自然语言写成并自动执行,通常仍是 UI Acceptance Test;“订单接口在指定状态下返回整数 totalCents,且现有 Consumer 能解析”才具有接口契约测试的目标。Gherkin 并不决定测试处在哪一层,Step Definition 才连接实际被测对象。Cucumber Step Definitions
Promptfoo 的 llm-rubric 是后一类方案的具体例子:以自然语言给出评分标准,由模型评估输出。这里自然语言并非没有判定器,而是采用了模型作为判定器。与确定性断言相比,它需要额外管理评分规则、模型配置、输入证据和误判;即使输出一个布尔值,也不意味着判定已经确定化。Promptfoo LLM Rubric
工程上可以将这套方法称为“行为契约验证”,但应明确这是团队采用的广义称呼,不能据此宣称已经完成接口兼容性验证。精确字段、权限拒绝、数据条数和状态转换优先使用确定性断言;“解释是否清晰”这类语义要求可用模型评分,并用人工标注的正反例校准。验证代码功能时应提供实际运行结果、页面状态或持久化证据,不能只给 Judge 阅读编码 Agent 的完成报告。
Agent 编码让契约的用途怎样变化#
这里需要区分两个对象:Agent 编写的软件可以用常规 Unit、Contract、Integration 和 E2E Test 验证;Agent 本身的行动与回答则可能需要工具调用轨迹断言与 Evals。让 Codex 写普通应用,不意味着应用的测试也必须使用 LLM Judge。
下面是面向 Agent 编码的工程建议,不是新的行业测试分类:
- 把意图变成先于实现的验收基准。给关键要求分配稳定 ID,写清前置状态、动作、可观察结果和错误场景,再让 Agent 映射到合适的测试。无需机械地把每个小改动都变成完整 TDD 流程。
- 在接口边界约束生成的实现。客户端和服务端分开实现时,共享可验证的交互约定,并分别验证真实 Consumer 与 Provider。若旧版客户端仍在使用,不能只验证同一次改动后的新客户端和新服务端;还应覆盖仍需支持的 Consumer 契约。
- 让 Agent 寻找反例。要求它设计缺字段、错误类型、越权、超时或重试场景,并明确哪些属于接口契约,哪些属于业务集成验证。回归修复应证明新增测试在未修复版本上失败,修复后通过;必要时用人为注入的错误检查测试是否敏感。
- 保持验收标准的独立性。可以让同一个 Agent 编码和补测试,但降低断言强度、跳过测试或更新预期值,需要单独审阅并说明原因。第二个 Agent 可以辅助审阅,但不构成独立正确性的保证。
- 在 CI 中保留可复查证据。记录被测提交、实际运行的测试和结果;关键兼容性检查应成为 Required Check。仅在 Prompt 写“必须通过测试”并不能阻止不合格代码合入。
例如,可以给编码 Agent 以下任务模板。它是自然语言验收说明,不是可以直接运行的测试文件:
目标:在 iOS 和 Android 实现订单查询功能。
AC-API-01:订单存在时,Consumer 能解析字符串 id 与整数 totalCents;
totalCents 以分计。Contract 同时验证真实 Consumer 与 Provider。
AC-API-02:订单不存在时,返回 404 与约定的错误 code。
AC-UI-01:加载失败时显示错误状态和重试入口,不显示成功状态。
AC-DATA-01:无权限用户不能读取订单内容;以真实权限边界的集成测试验证。
AC-CROSS-01:两端按同一份状态用例区分加载、成功、空结果与失败;
各端调用真实业务模块,平台 UI 分别验收。
先列出每个 AC 的测试层级与可观察证据,再实现。
固定共享用例版本,核对两端应运行的 Case ID 与实际运行结果。
保留仍需支持的旧 Consumer 契约。
若实现与约定冲突,列出冲突,不通过删除断言或更新预期值掩盖。
交付时报告实际执行命令、结果,以及未覆盖的场景。在需要精确重现“未授权时没有发生写入”或“重试没有重复创建订单”的场景中,应直接检查状态与副作用;这类证据不能由 Response Shape 或模型的自然语言判断替代。
选择起点与维护成本#
如果主要问题是 iOS / Android 业务规则漂移,先从一项高价值功能开始,共享少量覆盖边界的测试向量,让各端现有测试调用真实代码。无需先搭建庞大的跨平台框架。
当客户端与服务端独立发布、Mock 经常与真实响应不一致时,再引入 Consumer / Provider 契约验证。只有服务端对 Specification 的校验时,也应如实报告其覆盖范围,不能把它当成已验证所有客户端。
对于权限拒绝、金额、数据隔离和持久化次数等明确结果,优先采用确定性断言。自然语言评分适合语义目标,但要固定评分标准与模型配置,用人工标注的正反例校准,并保留实际运行证据。编码 Agent 的完成报告本身不是被测系统的行为证据。
关联阅读#
Agent 主导开发的测试概念:从验收规格到反例、状态与评估:了解性质、变异、状态模型与故障注入如何补充契约验证。
前端 UI 测试全景:Vitest、Playwright、视觉回归与 Agent 契约:Web Component、Browser Flow、视觉与可访问性验证。