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在缓存工程中的契约定义与验证流程:

契约定义与验证流程 契约定义与验证流程(Pact / Spring Cloud Contract) Pact 消费者驱动流程 ① 消费者编写Pact测试 ② 生成Pact契约文件 ③ 发布到Pact Broker ④ 提供者拉取并验证 ⑤ 验证结果回传Broker Spring Cloud Contract 提供者驱动流程 ① 提供者编写Groovy契约 ② 自动生成提供者测试 ③ 生成Stub JAR并发布 ④ 消费者使用Stub Runner ⑤ 消费者测试通过即契约生效

4.6 契约版本管理与兼容性

在数据库缓存工程中,缓存接口的演进频率较高(如新增缓存字段、调整TTL策略)。契约版本管理必须跟上迭代节奏:

场景 Pact处理方式 Spring Cloud Contract处理方式
新增可选字段 消费者契约添加字段,提供者验证通过即可 提供者契约添加字段,消费者Stub自动更新
删除字段 需标记为废弃,所有消费者移除后提供者方可删除 提供者删除字段,消费者需同步更新Stub版本
字段类型变更 视为不兼容变更,需协商后同时发布新版本 提供者发布新版本,消费者选择升级
缓存状态码新增 消费者定义新交互,提供者实现后验证 提供者定义新契约,消费者按需使用
版本兼容性红线:任何导致消费者测试失败的契约变更,必须暂停回归冒烟流水线,直到双方达成一致并更新契约。禁止在未通过契约验证的情况下直接修改缓存接口实现。

4.7 契约定义检查清单

在将契约纳入回归冒烟流水线之前,请逐项确认以下检查点:

  • 完整性:所有缓存操作(查询、预热、失效、批量)是否都有对应的契约定义?
  • 边界覆盖:是否包含缓存未命中、空缓存值、TTL过期等边界交互?
  • 状态预置:提供者端的@State或given()是否真实模拟了缓存数据状态?
  • 版本对齐:消费者与提供者使用的契约版本是否一致(通过Broker或Git标签校验)?
  • 自动化集成:契约验证是否已集成到CI/CD流水线中,作为回归冒烟的必经阶段?
总结:契约定义是契约测试驱动数据校验的基石。无论是Pact的消费者驱动还是Spring Cloud Contract的提供者驱动,核心目标都是让缓存服务间的数据交互变得可预测、可验证、可追溯。下一阶段将基于这些契约,构建回归冒烟测试用例并集成到持续验证流水线中。