4. 消费者端契约编写:使用 Pact Python 库定义消费者期望、模拟提供者响应、生成契约文件
在微服务架构中,消费者驱动的契约测试要求消费者(Consumer)首先明确声明其对提供者(Provider)API 的调用期望。本章将使用 Pact Python 库,演示如何编写消费者端测试,定义期望的请求与响应,模拟提供者行为,并最终生成可共享的契约文件(Pact 文件)。
4.1 消费者契约测试的核心流程
消费者端契约测试遵循“定义期望 → 模拟交互 → 验证匹配 → 生成契约”的闭环。下图展示了这一流程中各个组件的关系:
4.2 环境准备与依赖安装
在开始编写契约之前,确保已安装 pact-python 库。推荐在虚拟环境中进行:
# 创建并激活虚拟环境(可选)
python -m venv pact_env
source pact_env/bin/activate # Windows: pact_env\Scripts\activate
# 安装 pact-python
pip install pact-python
# 验证安装
pact --version
版本建议: 推荐使用
pact-python 2.x 版本,与 Pact 规范 v4 兼容。若项目使用异步框架(如 FastAPI),可额外安装 pact-python[async]。
4.3 定义消费者期望:以“聚合查询错误码”为例
假设我们的消费者服务需要调用提供者的 /api/v1/error-summary 接口,查询某时间段内的错误码聚合数据。消费者期望的交互如下:
| 交互要素 | 定义 |
|---|---|
| 请求方法 | GET |
| 请求路径 | /api/v1/error-summary |
| 查询参数 | start_date=2025-01-01&end_date=2025-01-31&service=auth |
| 期望状态码 | 200 |
| 期望响应体 | 包含 error_code、count、severity 字段的数组 |
| 期望响应头 | Content-Type: application/json |
使用 Pact Python 的 Consumer 和 Provider 类来定义上述交互:
import atexit
from pact import Consumer, Provider
# 1. 定义消费者和提供者
pact = Consumer('ErrorQueryService').has_pact_with(
Provider('ErrorAggregator'),
pact_dir='./pacts' # 契约文件输出目录
)
# 2. 启动 Mock Provider(在测试开始前)
pact.start_service()
atexit.register(pact.stop_service) # 确保测试结束后停止
# 3. 定义交互期望
expected_response = [
{
'error_code': 'AUTH_401',
'count': 150,
'severity': 'high'
},
{
'error_code': 'AUTH_403',
'count': 32,
'severity': 'medium'
}
]
(pact
.given('存在错误码聚合数据')
.upon_receiving('查询指定时间段的错误码聚合')
.with_request(
method='GET',
path='/api/v1/error-summary',
query={
'start_date': '2025-01-01',
'end_date': '2025-01-31',
'service': 'auth'
}
)
.will_respond_with(
status=200,
headers={'Content-Type': 'application/json'},
body=expected_response
))
关键点解析:
.given()定义提供者状态(Provider State),用于在提供者端测试时设置前置数据。.upon_receiving()描述消费者场景,便于后续追溯。.with_request()精确匹配请求方法、路径、查询参数和头信息。.will_respond_with()定义 Mock Provider 返回的固定响应。
4.4 模拟提供者响应与执行测试
定义期望后,需要编写消费者业务逻辑,并验证其与 Mock Provider 的交互是否符合预期。以下是一个完整的测试用例:
import requests
import unittest
class TestErrorQueryService(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.mock_url = 'http://localhost:1234' # Pact Mock Provider 默认端口
cls.pact = pact
def test_query_error_summary(self):
"""测试消费者正确解析聚合错误码响应"""
# 实际业务调用(消费者代码)
response = requests.get(
f'{self.mock_url}/api/v1/error-summary',
params={
'start_date': '2025-01-01',
'end_date': '2025-01-31',
'service': 'auth'
},
headers={'Accept': 'application/json'}
)
# 验证状态码
self.assertEqual(response.status_code, 200)
# 验证响应体结构
data = response.json()
self.assertIsInstance(data, list)
self.assertGreater(len(data), 0)
# 验证字段完整性
for item in data:
self.assertIn('error_code', item)
self.assertIn('count', item)
self.assertIn('severity', item)
# 验证具体值(可选)
self.assertEqual(data[0]['error_code'], 'AUTH_401')
def tearDown(self):
# 每次测试后验证交互是否完全匹配
self.pact.verify()
⚠️ 常见陷阱:
- 请求不匹配: 消费者发送的请求必须与
.with_request()完全一致(包括查询参数顺序、头信息)。Pact 默认使用严格匹配。 - 未调用 verify(): 忘记调用
pact.verify()会导致未使用的期望被忽略,契约文件可能不完整。 - 端口冲突: 确保 Mock Provider 端口未被占用,建议在 CI 环境中使用随机端口。
4.5 生成契约文件
测试通过后,Pact 库会自动将消费者期望写入 JSON 格式的契约文件。文件默认生成在 pact_dir 指定的目录下,命名规则为 {consumer}-{provider}.json。
以下是一个生成的契约文件片段:
{
"consumer": {
"name": "ErrorQueryService"
},
"provider": {
"name": "ErrorAggregator"
},
"interactions": [
{
"description": "查询指定时间段的错误码聚合",
"providerState": "存在错误码聚合数据",
"request": {
"method": "GET",
"path": "/api/v1/error-summary",
"query": {
"start_date": "2025-01-01",
"end_date": "2025-01-31",
"service": "auth"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": [
{
"error_code": "AUTH_401",
"count": 150,
"severity": "high"
},
{
"error_code": "AUTH_403",
"count": 32,
"severity": "medium"
}
]
}
}
],
"metadata": {
"pactSpecification": {
"version": "4.0"
}
}
}
4.6 契约文件的结构与字段说明
| 字段 | 说明 | 示例值 |
|---|---|---|
consumer.name |
消费者服务名称 | ErrorQueryService |
provider.name |
提供者服务名称 | ErrorAggregator |
interactions[].description |
交互场景描述 | 查询指定时间段的错误码聚合 |
interactions[].providerState |
提供者前置状态 | 存在错误码聚合数据 |
interactions[].request |
消费者请求定义 | method, path, query, headers |
interactions[].response |
提供者响应定义 | status, headers, body |
metadata.pactSpecification.version |
Pact 规范版本 | 4.0 |
4.7 使用匹配器增强契约灵活性
在实际项目中,响应体中的某些字段(如时间戳、自增 ID)可能每次不同。Pact 提供匹配器(Matchers)来定义灵活约束,避免契约过于脆弱。
以下示例使用 term 匹配器验证错误码格式,使用 each_like 匹配器验证数组结构:
from pact import Term, EachLike
# 定义匹配器
error_code_matcher = Term(
matcher=r'^[A-Z]+_\d{3}$', # 正则:大写字母+下划线+三位数字
generate='AUTH_401' # 生成示例值
)
severity_matcher = Term(
matcher=r'^(high|medium|low)$',
generate='high'
)
# 使用匹配器定义响应体
flexible_response = EachLike({
'error_code': error_code_matcher,
'count': Term(matcher=r'^\d+$', generate=150),
'severity': severity_matcher
}, min=1) # 至少包含 1 个元素
(pact
.given('存在错误码聚合数据')
.upon_receiving('查询错误码聚合(使用匹配器)')
.with_request(
method='GET',
path='/api/v1/error-summary',
query={'start_date': '2025-01-01', 'end_date': '2025-01-31'}
)
.will_respond_with(
status=200,
headers={'Content-Type': 'application/json'},
body=flexible_response
))
匹配器使用建议:
- 对枚举值、固定格式字段(如错误码、日期)使用
Term正则匹配。 - 对数组结构使用
EachLike,并设置min参数控制最小元素数。 - 避免过度使用匹配器,关键业务字段建议使用精确值以确保契约严谨性。
4.8 消费者契约编写最佳实践
4.9 本章小结
通过本章学习,您已经掌握了使用 Pact Python 编写消费者端契约的核心技能:
- 定义期望: 使用
.with_request()和.will_respond_with()精确描述消费者与提供者的交互。 - 模拟提供者: Pact Mock Provider 在测试期间模拟提供者行为,无需真实服务。
- 生成契约: 测试通过后自动生成标准化的 Pact 文件,作为消费者与提供者之间的“协议”。
- 灵活匹配: 使用 Term、EachLike 等匹配器处理动态字段,平衡契约的严谨性与灵活性。
在下一阶段,提供者将根据本章生成的契约文件进行验证,确保其 API 实现与消费者期望保持一致。