4. 消费者端契约编写:使用 Pact Python 库定义消费者期望、模拟提供者响应、生成契约文件

在微服务架构中,消费者驱动的契约测试要求消费者(Consumer)首先明确声明其对提供者(Provider)API 的调用期望。本章将使用 Pact Python 库,演示如何编写消费者端测试,定义期望的请求与响应,模拟提供者行为,并最终生成可共享的契约文件(Pact 文件)。

4.1 消费者契约测试的核心流程

消费者端契约测试遵循“定义期望 → 模拟交互 → 验证匹配 → 生成契约”的闭环。下图展示了这一流程中各个组件的关系:

消费者契约测试核心流程 消费者测试 定义期望请求/响应 Pact Mock Provider 模拟提供者行为 验证交互 断言状态/响应体 发送请求 返回响应 生成 Pact 文件 consumer-provider.json 测试通过后 共享契约 Pact Broker / 文件 💡 关键原则 消费者定义契约,提供者验证契约

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 消费者契约编写最佳实践

消费者契约编写最佳实践 1. 场景覆盖 覆盖正常、边界、异常场景 至少包含成功与失败路径 2. 精确匹配 请求路径、参数、头信息 必须与生产代码一致 3. 合理使用匹配器 对动态字段使用 Term 对数组使用 EachLike 4. 状态管理 使用 .given() 定义前置状态 状态名称应清晰可读 5. 验证完整性 每次测试后调用 verify() 确保所有期望被使用 6. 契约版本管理 将 Pact 文件纳入版本控制 配合 Pact Broker 管理

4.9 本章小结

通过本章学习,您已经掌握了使用 Pact Python 编写消费者端契约的核心技能:

  • 定义期望: 使用 .with_request() 和 .will_respond_with() 精确描述消费者与提供者的交互。
  • 模拟提供者: Pact Mock Provider 在测试期间模拟提供者行为,无需真实服务。
  • 生成契约: 测试通过后自动生成标准化的 Pact 文件,作为消费者与提供者之间的“协议”。
  • 灵活匹配: 使用 Term、EachLike 等匹配器处理动态字段,平衡契约的严谨性与灵活性。

在下一阶段,提供者将根据本章生成的契约文件进行验证,确保其 API 实现与消费者期望保持一致。