4. Pact 实战(上):搭建 Pact Broker 与编写第一个消费者端契约测试

本章进入 Pact 框架的实战环节。你将完成两件核心工作:部署 Pact Broker(契约共享与版本管理的基础设施)和编写第一个消费者驱动的契约测试(Java / Spring Boot 示例)。通过本章学习,你将掌握从测试编写到契约发布的全链路操作。

4.1 为什么需要 Pact Broker?

在单机或单分支场景下,Pact 文件可以保存在本地文件系统。一旦团队规模扩大、微服务数量增多,就需要一个中央仓库来管理契约文件、版本关系和验证状态。Pact Broker 正是为此而生:

能力 本地文件 Pact Broker
契约存储 单机目录 中央仓库,支持多团队
版本管理 手动命名 自动关联消费者/提供者版本
验证触发 手动复制 Webhook 自动通知提供者
矩阵查询 不支持 查看哪些版本已验证
环境标记 不支持 标记生产/测试环境
推荐策略:即使只有 2 个微服务,也建议从第一天起使用 Pact Broker。后期迁移成本远高于初期搭建成本。

4.2 搭建 Pact Broker(Docker Compose 方式)

Pact Broker 官方推荐使用 Docker 部署。下面是一个最小化生产级配置,包含 Broker 服务和一个 PostgreSQL 数据库。

4.2.1 docker-compose.yml

version: '3.8'

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: pact
      POSTGRES_PASSWORD: pact_pass
      POSTGRES_DB: pact_broker
    volumes:
      - pgdata:/var/lib/postgresql/data

  broker:
    image: pactfoundation/pact-broker:latest
    ports:
      - "9292:9292"
    environment:
      PACT_BROKER_DATABASE_USERNAME: pact
      PACT_BROKER_DATABASE_PASSWORD: pact_pass
      PACT_BROKER_DATABASE_HOST: postgres
      PACT_BROKER_DATABASE_NAME: pact_broker
      PACT_BROKER_PORT: "9292"
      PACT_BROKER_DATABASE_CONNECT_MAX_RETRIES: "10"
    depends_on:
      - postgres

volumes:
  pgdata:

4.2.2 启动与验证

  1. 在 docker-compose.yml 所在目录执行:docker-compose up -d
  2. 等待约 30 秒,访问 http://localhost:9292
  3. 首次访问会进入初始化页面,设置管理员账号(建议使用 admin / admin 用于本地开发)
  4. 登录后看到空白的 Pact 列表页面,即表示 Broker 运行正常
生产环境注意:请务必修改默认密码,并配置 HTTPS 和持久化卷的备份策略。PostgreSQL 连接字符串中的密码不应硬编码在 compose 文件中。

4.3 消费者端契约测试架构

在编写测试之前,先理解消费者端测试在整个契约测试流程中的位置:

消费者端契约测试流程 消费者服务 (Order Service) Pact 契约测试 (JUnit 5 + Pact) Mock Provider (模拟接口) Pact Broker (契约仓库) 调用 模拟交互 返回响应 发布契约文件 关键说明 1. 消费者测试使用 Mock Provider 模拟真实提供者行为 2. 测试通过后生成 JSON 格式的 Pact 契约文件 3. 契约文件自动发布到 Pact Broker,供提供者端验证

4.4 编写第一个消费者端契约测试

我们以 Order Service(消费者)调用 User Service(提供者)的 /users/{id} 接口为例,演示完整的测试编写过程。

4.4.1 项目依赖(pom.xml)

<!-- Spring Boot Starter Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Pact Consumer JUnit 5 -->
<dependency>
    <groupId>au.com.dius.pact.consumer</groupId>
    <artifactId>junit5</artifactId>
    <version>4.6.3</version>
    <scope>test</scope>
</dependency>

<!-- Pact Consumer 核心库 -->
<dependency>
    <groupId>au.com.dius.pact.consumer</groupId>
    <artifactId>java8</artifactId>
    <version>4.6.3</version>
    <scope>test</scope>
</dependency>

4.4.2 消费者服务代码

首先定义消费者侧调用提供者的 Feign 客户端(或 RestTemplate):

// UserServiceClient.java
@Component
public class UserServiceClient {

    private final RestTemplate restTemplate;
    private final String userServiceUrl;

    public UserServiceClient(RestTemplate restTemplate,
                             @Value("${user.service.url}") String userServiceUrl) {
        this.restTemplate = restTemplate;
        this.userServiceUrl = userServiceUrl;
    }

    public User getUserById(Long id) {
        String url = userServiceUrl + "/users/" + id;
        ResponseEntity<User> response = restTemplate.getForEntity(url, User.class);
        if (response.getStatusCode() == HttpStatus.OK) {
            return response.getBody();
        }
        throw new RuntimeException("Failed to get user: " + response.getStatusCode());
    }
}

4.4.3 编写 Pact 契约测试

这是本章的核心——使用 @PactTestFor 和 @Pact 注解定义消费者期望:

// UserServiceClientPactTest.java
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "UserService", port = "8081")
public class UserServiceClientPactTest {

    @Pact(consumer = "OrderService")
    public V4Pact createUserPact(PactDslWithProvider builder) {
        return builder
            .given("用户 ID 1001 存在")
            .uponReceiving("获取用户信息")
                .path("/users/1001")
                .method("GET")
            .willRespondWith()
                .status(200)
                .headers(Map.of("Content-Type", "application/json"))
                .body(new PactDslJsonBody()
                    .integerType("id", 1001)
                    .stringType("name", "张三")
                    .stringType("email", "zhangsan@example.com")
                )
            .toPact(V4Pact.class);
    }

    @Test
    @PactTestFor(pactMethod = "createUserPact")
    void testGetUserById(MockServer mockServer) {
        // 使用 MockServer 的 URL 替换真实提供者地址
        UserServiceClient client = new UserServiceClient(
            new RestTemplate(),
            mockServer.getUrl()
        );

        User user = client.getUserById(1001L);

        assertThat(user.getId()).isEqualTo(1001);
        assertThat(user.getName()).isEqualTo("张三");
        assertThat(user.getEmail()).isEqualTo("zhangsan@example.com");
    }
}
测试要点解析:
  • @Pact(consumer = "OrderService"):定义消费者名称,必须与提供者端配置一致
  • given("用户 ID 1001 存在"):提供者端的状态描述,用于提供者测试时匹配
  • uponReceiving("获取用户信息"):测试场景描述,出现在 Pact Broker 的交互记录中
  • PactDslJsonBody:使用 DSL 定义 JSON 结构,支持类型匹配和正则约束
  • mockServer.getUrl():Pact 自动启动 Mock Server,测试结束后自动关闭

4.4.4 运行测试并生成 Pact 文件

执行测试后,在 target/pacts/ 目录下会生成 OrderService-UserService.json 文件。以下是生成的契约文件片段:

{
  "consumer": { "name": "OrderService" },
  "provider": { "name": "UserService" },
  "interactions": [
    {
      "description": "获取用户信息",
      "providerStates": [
        { "name": "用户 ID 1001 存在" }
      ],
      "request": {
        "method": "GET",
        "path": "/users/1001",
        "headers": { "Accept": "application/json" }
      },
      "response": {
        "status": 200,
        "headers": { "Content-Type": "application/json" },
        "body": {
          "id": 1001,
          "name": "张三",
          "email": "zhangsan@example.com"
        }
      }
    }
  ],
  "metadata": {
    "pactSpecification": { "version": "4.0" }
  }
}

4.5 将契约发布到 Pact Broker

测试通过后,需要将生成的 Pact 文件发布到 Broker。推荐使用 Maven 插件或 Gradle 任务自动化发布。

4.5.1 Maven 插件配置

<plugin>
    <groupId>au.com.dius.pact.provider</groupId>
    <artifactId>maven</artifactId>
    <version>4.6.3</version>
    <configuration>
        <pactBrokerUrl>http://localhost:9292</pactBrokerUrl>
        <pactBrokerUsername>admin</pactBrokerUsername>
        <pactBrokerPassword>admin</pactBrokerPassword>
        <projectVersion>1.0.0</projectVersion>
        <trimSnapshot>true</trimSnapshot>
    </configuration>
</plugin>

4.5.2 执行发布

mvn pact:publish -Dpact.consumer.version=1.0.0

发布成功后,访问 http://localhost:9292 即可看到 OrderService 与 UserService 的契约记录。

4.6 消费者测试最佳实践

实践 说明
每个接口一个 @Pact 方法 保持测试粒度清晰,便于定位问题
使用 providerState 描述前置条件 提供者端根据 state 准备测试数据
避免硬编码具体值 使用 integerType()、stringType() 等 DSL 方法定义类型匹配
测试覆盖异常路径 如 404、500 响应,确保消费者正确处理错误
CI 中自动发布 每次构建成功后自动发布新版本契约
常见陷阱:不要在消费者测试中直接使用真实数据库或外部服务。Pact 的 Mock Server 已经模拟了提供者行为,任何外部依赖都会破坏测试的隔离性和可重复性。

4.7 验证发布结果

回到 Pact Broker 的 Web 界面,你应该能看到:

  • 左侧导航出现 OrderService 和 UserService 两个应用
  • 点击 UserService 可以看到来自 OrderService 的契约
  • 契约详情页显示交互列表、请求/响应详情和元数据
  • 版本号 1.0.0 已关联到该契约