Pact 契约测试:消费者驱动的 API 验证
什么是 Pact 契约测试
Pact 是一种消费者驱动的契约测试框架,主要用于验证微服务或分布式系统中不同服务之间 API 交互的正确性。它通过让服务消费者定义期望的请求与响应(即契约),再由服务提供者验证其实现是否符合该契约,从而确保双方集成时不会出现破坏性变更。
契约测试不同于传统的端到端测试,它专注于服务间的接口约定,测试速度快、独立性高,能够高效定位集成问题。
核心概念
消费者与提供者
- 消费者 (Consumer):调用 API 的服务或应用,它定义期望的请求和响应。
- 提供者 (Provider):提供 API 的服务,它根据消费者定义的契约验证自身实现。
契约 (Pact)
契约是一份由消费者生成的 JSON 文件,记录了交互场景下具体的 HTTP 请求格式和预期的响应内容,包括路径、方法、请求头、请求体、响应状态码、响应头、响应体等。
Mock Service
消费者测试时,Pact 会启动一个本地的模拟服务,用来代替真实的提供者返回契约中定义的响应,从而验证消费者代码能否正确处理这些响应。
契约验证
提供者端运行测试时,Pact 会根据契约文件向真实提供者发送请求,并检查实际响应是否与契约完全匹配。
环境准备
技术栈选择
Pact 支持多种语言,本教程以 JavaScript/Node.js 为例,其他语言用法类似。
安装依赖:
npm install --save-dev @pact-foundation/pact @pact-foundation/pact-node
需要 Node.js 环境 (v10+),可选安装 Pact Broker 用于共享契约(可后续引入)。
消费者端测试
消费者端测试的核心是定义契约并验证消费者代码能否基于该契约正确运行。
1. 定义消费者代码
假设有一个 UserServiceClient 通过 GET /api/users/1 获取用户信息:
// userClient.js
const axios = require('axios');
class UserServiceClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async getUser(id) {
const response = await axios.get(`${this.baseUrl}/api/users/${id}`);
return response.data;
}
}
module.exports = UserServiceClient;
2. 编写 Pact 测试
在测试中定义一个交互,并使用 mock service 验证客户端行为:
// userClient.pact.test.js
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const path = require('path');
const UserServiceClient = require('./userClient');
const { like } = MatchersV3;
describe('Pact V3 Consumer Test', () => {
let provider;
beforeAll(() => {
provider = new PactV3({
consumer: 'UserWeb',
provider: 'UserAPI',
dir: path.resolve(process.cwd(), 'pacts'),
});
});
it('can fetch a user', () => {
// 设置交互的期望
provider
.given('user with id 1 exists')
.uponReceiving('a request to get user 1')
.withRequest({
method: 'GET',
path: '/api/users/1',
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: like({
id: 1,
name: 'Alice',
}),
});
// 在 mock 服务环境下运行客户端代码
return provider.executeTest(async (mockServer) => {
const client = new UserServiceClient(mockServer.url);
const user = await client.getUser(1);
expect(user).toEqual({
id: 1,
name: 'Alice',
});
});
});
});
运行测试后,pacts 目录下会生成 UserWeb-UserAPI.json 契约文件。
匹配器 (Matchers)
like(value):表示期望值是某种类型,具体值在提供者端验证时只要类型匹配即可。eachLike(content):数组元素匹配器。string(),integer(),boolean()等精确类型匹配器。
提供者端验证
提供者负责从契约文件中读取期望,并验证真实 API 是否满足要求。
1. 提供者 API 示例
假设提供者是一个 Express 应用:
// provider.js
const express = require('express');
const app = express();
app.get('/api/users/:id', (req, res) => {
res.json({
id: Number(req.params.id),
name: 'Alice',
});
});
module.exports = app;
2. 编写提供者验证测试
使用 @pact-foundation/pact 提供的 Verifier:
// provider.pact.test.js
const { VerifierV3 } = require('@pact-foundation/pact');
const path = require('path');
const app = require('./provider');
describe('Pact V3 Provider Verification', () => {
let server;
beforeAll(() => {
server = app.listen(3000, () => {
console.log('Provider listening on port 3000');
});
});
afterAll(() => {
server.close();
});
it('validates the expectations of UserWeb', () => {
return new VerifierV3({
provider: 'UserAPI',
providerBaseUrl: 'http://localhost:3000',
pactUrls: [
path.resolve(__dirname, '../pacts/UserWeb-UserAPI.json'),
],
}).verifyProvider();
});
});
执行测试,Pact 会向 http://localhost:3000/api/users/1 发送请求,将响应与契约进行对比。全部匹配则验证通过。
工作流与集成
消费者驱动流程
- 消费者团队先编写测试,定义所需契约。
- 消费者测试通过后,将生成的契约文件共享给提供者团队(通常通过 Pact Broker)。
- 提供者团队拉取契约,运行提供者验证测试。
- 验证通过后,双方可安全集成。
Pact Broker
Pact Broker 是一个用于管理和交换契约的服务,可以记录契约版本、验证结果,并形成“契约网络”。
- 消费者在 CI 中将契约发布到 Broker。
- 提供者从 Broker 拉取最新的契约进行验证。
- 提供者验证结果也会回传至 Broker,形成集成状态看板。
可以自行搭建 Broker,或使用 pactflow.io 提供的托管服务。
匹配规则详解
请求匹配
- 路径:可包含参数,如
/api/users/{id},使用MatchersV3.string('id')匹配路径参数。 - 查询参数:
query: { status: 'active' },支持匹配器。 - 请求头:
headers: { 'Content-Type': 'application/json' }。
响应匹配
- 状态码必须完全一致。
- 响应头可按需匹配,未指定的头会被忽略。
- body 可使用
like(),eachLike(),term()等灵活匹配,避免提供者返回额外字段或不同值的失败。
Provider State
given() 用于描述提供者的前置状态,如“用户 ID 1 存在”。提供者端需要实现状态回调机制(通过 stateHandlers)来准备测试数据。
常见问题与最佳实践
契约不要过于严格
过度使用精确值会导致提供者任何微小变化(如增加字段)都破坏契约。推荐广泛使用匹配器定义结构而非具体值。
状态管理
提供者应实现 setupProviderState 方法,以确保测试时数据库等资源符合 given 描述的初始状态。
消费者测试的独立性
消费者测试不依赖真实提供者,因此运行极快,适合在开发阶段频繁执行。
契约版本控制
将生成的契约文件纳入版本管理,或通过 Broker 管理版本,确保提供者总是验证正确的版本。
持续集成
- 消费者 CI:运行测试 → 发布契约至 Broker。
- 提供者 CI:从 Broker 拉取契约 → 运行验证 → 回传结果。
- 可以在 Broker 上配置 webhook 触发提供者验证。
运行与调试
查看日志
消费者测试时设置 logLevel: 'debug' 可查看 mock 服务的请求匹配详情。
处理验证失败
验证失败会给出详细差异信息,包括缺失字段、类型不匹配、状态码错误等,根据差异调整提供者实现或更新契约。
总结
Pact 契约测试将 API 集成风险前移,消费者明确表达需求,提供者据此验证实现,实现“消费者驱动”的设计与测试闭环。它让微服务团队能够独立演进,同时保证接口兼容性,是构建高可靠分布式系统的重要实践。