4. 契约定义:使用Pact或Spring Cloud Contract定义服务间数据交互契约
在数据库缓存工程中,服务间数据交互的稳定性直接决定了缓存一致性能否达成。契约测试的核心在于先定义、后实现——服务消费者与提供者就接口的请求/响应格式、字段约束、边界条件达成书面约定,再各自独立开发与验证。本章聚焦两种主流契约框架:Pact(消费者驱动)与Spring Cloud Contract(提供者驱动),并给出在缓存场景下的具体定义方法。
核心原则:契约是双向的“数据校验合同”。消费者定义“我需要什么”,提供者承诺“我提供什么”。任何一方的变更都必须通过契约验证才能进入回归冒烟流水线。
4.1 契约框架选型对比
| 维度 | Pact | Spring Cloud Contract |
|---|---|---|
| 驱动方向 | 消费者驱动(Consumer-Driven) | 提供者驱动(Provider-Driven) |
| 契约存储 | Pact Broker(中央仓库) | Git仓库 + Stub Runner |
| 语言生态 | 多语言(JVM、JS、Go、Python等) | 主要面向JVM(Spring生态) |
| 缓存场景适配 | 适合异步缓存更新、事件驱动接口 | 适合同步REST缓存查询、缓存预热接口 |
| 验证方式 | 消费者编写测试 → 提供者验证 | 提供者生成测试 → 消费者使用Stub |
| 版本管理 | 内置版本矩阵与兼容性校验 | 通过Git分支与标签管理 |
选型建议:若团队使用Spring技术栈且缓存接口以REST为主,优先选择Spring Cloud Contract;若涉及多语言微服务、异步缓存事件或需要消费者主动驱动契约演进,选择Pact更灵活。
4.2 使用Pact定义缓存查询契约
以下示例展示一个缓存键查询接口的Pact契约定义。消费者(订单服务)期望从提供者(缓存管理服务)获取指定缓存键的元数据。
4.2.1 消费者端契约编写(Java + JUnit 5)
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "cache-manager-service", port = "8080")
public class CacheQueryConsumerPactTest {
@Pact(consumer = "order-service")
public V4Pact createCacheQueryPact(PactDslWithProvider builder) {
return builder
.given("缓存键 'order:1001' 存在")
.uponReceiving("查询缓存键元数据请求")
.path("/api/cache/order:1001")
.method("GET")
.headers("Accept", "application/json")
.willRespondWith()
.status(200)
.headers("Content-Type", "application/json")
.body(new PactDslJsonBody()
.stringType("cacheKey", "order:1001")
.stringType("cacheValue", "{\"orderId\":1001,\"status\":\"PAID\"}")
.integerType("ttlSeconds", 3600)
.stringType("cacheStatus", "HIT")
)
.toPact(V4Pact.class);
}
@Test
@PactTestFor(pactMethod = "createCacheQueryPact")
public void testCacheQuery(MockServer mockServer) {
// 使用mockServer的URL发起真实HTTP调用
String response = new RestTemplate()
.getForObject(mockServer.getUrl() + "/api/cache/order:1001", String.class);
assertThat(response).contains("\"cacheStatus\":\"HIT\"");
}
}
4.2.2 提供者端契约验证
@Provider("cache-manager-service")
@PactBroker(url = "http://pact-broker:9292")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CacheQueryProviderPactTest {
@LocalServerPort
int port;
@BeforeEach
void setup(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
@State("缓存键 'order:1001' 存在")
public void setupCacheData() {
// 预置缓存数据,确保提供者能返回契约中定义的字段
cacheManager.put("order:1001", "{\"orderId\":1001,\"status\":\"PAID\"}", 3600);
}
}
缓存场景注意:Pact契约中的
cacheStatus字段(HIT/MISS)必须在提供者端真实模拟。若提供者返回MISS但契约期望HIT,验证将失败。建议在@State中明确预置缓存数据,确保状态可重复。
4.3 使用Spring Cloud Contract定义缓存预热契约
Spring Cloud Contract采用提供者驱动方式,提供者将契约定义在代码仓库中,消费者通过Stub Runner获取模拟响应。以下示例定义缓存批量预热接口的契约。
4.3.1 提供者端契约定义(Groovy DSL)
// contracts/cache/prewarmCache.groovy
Contract.make {
description "批量预热缓存键"
request {
method POST()
url "/api/cache/prewarm"
headers {
contentType applicationJson()
}
body([
keys: ["product:2001", "product:2002", "product:2003"],
ttlSeconds: 7200
])
}
response {
status OK()
headers {
contentType applicationJson()
}
body([
successCount: 3,
failedKeys: [],
totalElapsedMs: 45
])
}
}
4.3.2 提供者端自动生成测试
Spring Cloud Contract会根据上述契约自动生成验证测试类,提供者只需确保接口实现通过该测试:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.LOCAL,
ids = "com.example:cache-manager-service:+:stubs:8080")
public class CachePrewarmProviderTest {
@Autowired
private MockMvc mockMvc;
// 契约框架自动生成验证方法,无需手动编写
// 运行 mvn verify 时自动执行
}
4.3.3 消费者端使用Stub
@SpringBootTest
@AutoConfigureStubRunner(
stubsMode = StubRunnerProperties.StubsMode.CLASSPATH,
ids = "com.example:cache-manager-service:+:stubs:8080")
public class CachePrewarmConsumerTest {
@Autowired
private RestTemplate restTemplate;
@Test
public void testPrewarm() {
// Stub Runner 启动模拟服务,返回契约中定义的响应
String url = "http://localhost:8080/api/cache/prewarm";
PrewarmRequest request = new PrewarmRequest(
List.of("product:2001", "product:2002"),
7200
);
PrewarmResponse response = restTemplate.postForObject(url, request, PrewarmResponse.class);
assertThat(response.getSuccessCount()).isEqualTo(3);
assertThat(response.getFailedKeys()).isEmpty();
}
}
4.4 契约定义中的数据校验规则
在缓存场景中,契约定义必须覆盖以下数据校验维度,否则回归冒烟可能遗漏关键差异:
- 字段存在性:缓存键、缓存值、TTL、状态码等核心字段必须出现在契约中,不允许隐式省略。
- 类型约束:字符串、整数、布尔值必须明确指定类型,避免JSON序列化歧义。
- 边界值:空缓存值、TTL为0、键长度超限等边界情况应在契约中单独定义交互。
- 可选字段:使用
eachLike或optionalType标记可能缺失的字段(如缓存标签)。 - 响应状态码:200(命中)、404(未命中)、500(缓存异常)必须分别定义契约。
缓存契约最佳实践:为每个缓存操作(查询、预热、失效、批量查询)分别定义独立契约,避免将多个交互合并到一个契约文件中。这有助于在回归冒烟中精确定位哪个缓存操作的数据校验失败。
4.5 契约定义流程与SVG架构
下图展示Pact与Spring Cloud Contract在缓存工程中的契约定义与验证流程:
4.6 契约版本管理与兼容性
在数据库缓存工程中,缓存接口的演进频率较高(如新增缓存字段、调整TTL策略)。契约版本管理必须跟上迭代节奏:
| 场景 | Pact处理方式 | Spring Cloud Contract处理方式 |
|---|---|---|
| 新增可选字段 | 消费者契约添加字段,提供者验证通过即可 | 提供者契约添加字段,消费者Stub自动更新 |
| 删除字段 | 需标记为废弃,所有消费者移除后提供者方可删除 | 提供者删除字段,消费者需同步更新Stub版本 |
| 字段类型变更 | 视为不兼容变更,需协商后同时发布新版本 | 提供者发布新版本,消费者选择升级 |
| 缓存状态码新增 | 消费者定义新交互,提供者实现后验证 | 提供者定义新契约,消费者按需使用 |
版本兼容性红线:任何导致消费者测试失败的契约变更,必须暂停回归冒烟流水线,直到双方达成一致并更新契约。禁止在未通过契约验证的情况下直接修改缓存接口实现。
4.7 契约定义检查清单
在将契约纳入回归冒烟流水线之前,请逐项确认以下检查点:
- 完整性:所有缓存操作(查询、预热、失效、批量)是否都有对应的契约定义?
- 边界覆盖:是否包含缓存未命中、空缓存值、TTL过期等边界交互?
- 状态预置:提供者端的
@State或given()是否真实模拟了缓存数据状态? - 版本对齐:消费者与提供者使用的契约版本是否一致(通过Broker或Git标签校验)?
- 自动化集成:契约验证是否已集成到CI/CD流水线中,作为回归冒烟的必经阶段?
总结:契约定义是契约测试驱动数据校验的基石。无论是Pact的消费者驱动还是Spring Cloud Contract的提供者驱动,核心目标都是让缓存服务间的数据交互变得可预测、可验证、可追溯。下一阶段将基于这些契约,构建回归冒烟测试用例并集成到持续验证流水线中。