Pact案例

wen java案例 2

本文目录导读:

Pact案例

  1. 案例一:后端微服务调用(HTTP + Spring Boot + JUnit)
  2. 案例二:消息队列场景(Kafka / 异步消息)
  3. 关键点提示(Pact 的“坑”)

Pact 是一个用于消费者驱动契约测试(Consumer-Driven Contract Testing,CDCT) 的工具,它的核心思想是:在微服务架构中,由消费者(Consumer)定义它期望从提供者(Provider)那里获得什么样的响应,生成一份“契约”,然后由提供者(Provider)来验证这份契约是否被满足。

由于篇幅限制,我无法提供完整的代码库,但我可以为你提供 2 个最经典、最完整的实战案例,分别涵盖后端服务间调用(HTTP)前端与后端(Message/Stream)的场景。


后端微服务调用(HTTP + Spring Boot + JUnit)

场景:有一个 Order Service(消费者)需要从 User Service(提供者)获取用户信息。

项目结构

contract-testing
├── user-service (提供者)
│   ├── src/main/java
│   ├── src/test/java
│   └── pom.xml
└── order-service (消费者)
    ├── src/main/java
    ├── src/test/java
    └── pom.xml

消费者端(Order Service)

步骤:编写消费者测试,生成契约文件(JSON),并将其发布到 Pact Broker(或本地文件)。

a. 添加依赖(pom.xml)

<dependency>
    <groupId>au.com.dius.pact.consumer</groupId>
    <artifactId>junit5</artifactId>
    <version>4.5.6</version>
    <scope>test</scope>
</dependency>

b. 定义 FeignClient 接口

public interface UserServiceClient {
    @GetMapping("/users/{id}")
    User getUserById(@PathVariable("id") Long id);
}

c. 编写消费者测试(OrderServicePactTest.java)

import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.core.model.RequestResponsePact;
import au.com.dius.pact.core.model.annotations.Pact;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "UserService")
public class OrderServicePactTest {
    // 1. 定义契约
    @Pact(consumer = "OrderService")
    public RequestResponsePact createPact(PactDslWithProvider builder) {
        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/json");
        return builder
            .given("a user with ID 100 exists") // 提供者的状态
            .uponReceiving("A request for a user by ID") // 请求描述
            .path("/users/100")
            .method("GET")
            .willRespondWith()
            .status(200)
            .headers(headers)
            .body("{\"id\": 100, \"name\": \"Alice\", \"email\": \"alice@example.com\"}") // 期望的响应体
            .toPact();
    }
    // 2. 运行测试,Mock Server 会模拟提供者
    @Test
    @PactTestFor(pactMethod = "createPact")
    void testGetUserById(MockServer mockServer) {
        String url = mockServer.getUrl() + "/users/100";
        RestTemplate restTemplate = new RestTemplate();
        // 模拟调用 HTTP 请求
        User user = restTemplate.getForObject(url, User.class);
        assertThat(user.getId()).isEqualTo(100);
        assertThat(user.getName()).isEqualTo("Alice");
    }
}

d. 生成契约文件: 运行测试后,会在 target/pacts 目录下生成 OrderService-UserService.json 文件。


提供者端(User Service)

步骤:编写提供者测试,验证 Provider 是否符合契约。

a. 添加依赖(pom.xml)

<dependency>
    <groupId>au.com.dius.pact.provider</groupId>
    <artifactId>junit5</artifactId>
    <version>4.5.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>au.com.dius.pact.provider</groupId>
    <artifactId>spring</artifactId>
    <version>4.5.6</version>
    <scope>test</scope>
</dependency>

b. 编写 Provider 的单元测试(UserServiceProviderTest.java)

import au.com.dius.pact.provider.junit5.HttpTestTarget;
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
import au.com.dius.pact.provider.junitsupport.Provider;
import au.com.dius.pact.provider.junitsupport.State;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Provider("UserService") // 当前服务名,必须与消费者测试中的 providerName 一致
public class UserServiceProviderTest {
    @LocalServerPort
    int port;
    @BeforeEach
    void setUp(PactVerificationContext context) {
        // 指定 Pact Broker 地址(或本地文件路径)
        context.setTarget(new HttpTestTarget("localhost", port));
        context.setPactSource(new UrlPactSource("https://pact-broker.example.com/pacts/provider/UserService/latest"));
    }
    // 验证所有契约
    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void verifyPact(PactVerificationContext context) {
        context.verifyInteraction();
    }
    // 对应消费者中的 given("a user with ID 100 exists")
    @State("a user with ID 100 exists")
    public void userExistsState() {
        // 在这里可以准备测试数据(Mock 数据库)
    }
}

c. 运行测试: 运行 Provider 测试,User Service 返回的数据结构与契约不符(例如字段名变了),测试将失败。


消息队列场景(Kafka / 异步消息)

场景Payment Service 发送一个 PaymentEvent 消息到 Kafka,Analytics Service 订阅该消息。

消息定义(共享库)

public class PaymentEvent {
    private String paymentId;
    private BigDecimal amount;
    private String currency;
    // getters/setters...
}

消费者端(Analytics Service - 消费 Kafka 消息)

Pact 的消息测试不使用 Mock Server,而是使用 MessagePact

import au.com.dius.pact.consumer.MessagePactBuilder;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.core.model.messaging.MessagePact;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import java.util.HashMap;
import java.util.Map;
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "PaymentService")
public class AnalyticsServiceKafkaPactTest {
    @Pact(consumer = "AnalyticsService")
    public MessagePact createMessagePact(MessagePactBuilder builder) {
        PactDslJsonBody body = new PactDslJsonBody()
                .stringType("paymentId", "abc-123")
                .numberType("amount", 99.99)
                .stringType("currency", "USD");
        // 定义消息元数据(对于 Kafka 通常包含 topic / key)
        Map<String, String> meta = new HashMap<>();
        meta.put("kafka_topic", "payments");
        return builder
                .given("A payment has been processed")
                .expectsToReceive("A valid PaymentEvent")
                .withContent(body)
                .withMetadata(meta)
                .toPact();
    }
    @Test
    void testMessageProcessing() {
        // 这里主要验证契约生成,不需要真正消费
    }
}

提供者端(Payment Service - 发送 Kafka 消息)

提供者测试需要验证发送的消息是否符合契约。

import au.com.dius.pact.provider.junit5.MessageTestTarget;
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
import au.com.dius.pact.provider.junitsupport.Provider;
import au.com.dius.pact.provider.junitsupport.State;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
@Provider("PaymentService")
public class PaymentServiceKafkaProviderTest {
    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void verifyPact(PactVerificationContext context) {
        // 设置消息目标
        context.setTarget(new MessageTestTarget());
    }
    // 核心:模拟发送消息,返回给 Pact 验证
    @PactVerifyProvider("A valid PaymentEvent")
    public PaymentEvent paymentEventMessage() {
        // 模拟业务逻辑返回的 Payload
        PaymentEvent event = new PaymentEvent();
        event.setPaymentId("xyz-789");
        event.setAmount(new BigDecimal("100.50"));
        event.setCurrency("EUR");
        return event;
    }
    @State("A payment has been processed")
    public void paymentProcessed() {
        // 准备测试数据
    }
}

  1. 消费者编写测试:定义期望的 API 或消息格式。
  2. 生成契约文件*.json 文件。
  3. 发布契约:上传到 Pact Broker(如 pactflow.io 或自建),或通过 pact:publish 命令。
  4. 提供者拉取契约:从 Broker 下载最新的契约。
  5. 提供者验证:运行测试,确保实现符合契约。
  6. 持续集成:CI 管道中,任何一侧代码变更都会触发验证,如果合同破坏,构建失败(Can I Deploy? 检查)。

关键点提示(Pact 的“坑”)

  1. Pact 不是端到端测试:它只验证 API 的请求/响应格式,不验证业务逻辑的复杂性,也不负责验证数据是否正确写入数据库。
  2. 状态管理(Provider State):API 依赖数据库中的数据(如案例一中的 given),需要在 Provider 端使用 @State 设置好对应数据。
  3. 版本与兼容性:建议搭配 Pact Broker 使用,它会解决“消费者版本 vs 提供者版本”的兼容性问题。
  4. 注意匹配规则:在契约中使用 stringType()numberType() 等 DSL 而不是硬编码值,可以避免误判错误(本来动态变化的 UUID 写死导致验证失败)。

如果你需要具体的代码文件(pom.xml 完整配置或 User 实体类)来运行测试,可以告诉我你用的构建工具(Maven 或 Gradle)和语言(Java 或 Python),我可以为你生成可直接运行的完整项目骨架。

抱歉,评论功能暂时关闭!