本文目录导读:

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() {
// 准备测试数据
}
}
- 消费者编写测试:定义期望的 API 或消息格式。
- 生成契约文件:
*.json文件。 - 发布契约:上传到 Pact Broker(如
pactflow.io或自建),或通过pact:publish命令。 - 提供者拉取契约:从 Broker 下载最新的契约。
- 提供者验证:运行测试,确保实现符合契约。
- 持续集成:CI 管道中,任何一侧代码变更都会触发验证,如果合同破坏,构建失败(Can I Deploy? 检查)。
关键点提示(Pact 的“坑”)
- Pact 不是端到端测试:它只验证 API 的请求/响应格式,不验证业务逻辑的复杂性,也不负责验证数据是否正确写入数据库。
- 状态管理(Provider State):API 依赖数据库中的数据(如案例一中的
given),需要在 Provider 端使用@State设置好对应数据。 - 版本与兼容性:建议搭配 Pact Broker 使用,它会解决“消费者版本 vs 提供者版本”的兼容性问题。
- 注意匹配规则:在契约中使用
stringType()、numberType()等 DSL 而不是硬编码值,可以避免误判错误(本来动态变化的 UUID 写死导致验证失败)。
如果你需要具体的代码文件(pom.xml 完整配置或 User 实体类)来运行测试,可以告诉我你用的构建工具(Maven 或 Gradle)和语言(Java 或 Python),我可以为你生成可直接运行的完整项目骨架。