1. 引言

Spring Cloud Contract 是一个专门用于编写消费者驱动契约(CDC) 的项目。它的核心作用是确保分布式系统中生产者(Producer)和消费者(Consumer)之间的契约一致性,覆盖 HTTP 和消息两种交互场景。

本文将通过一个 HTTP 交互示例,演示如何使用 Spring Cloud Contract 编写生产者和消费者端的测试用例。

2. 生产者端(服务端)

我们创建一个简单的 EvenOddController 作为生产者端,用于判断传入的数字是奇数还是偶数:

@RestController
public class EvenOddController {

    @GetMapping("/validate/prime-number")
    public String isNumberPrime(@RequestParam("number") Integer number) {
        return Integer.parseInt(number) % 2 == 0 ? "Even" : "Odd";
    }
}

2.1. Maven 依赖

生产者端需要添加以下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <version>2.1.1.RELEASE</version>
    <scope>test</scope>
</dependency>

同时配置 spring-cloud-contract-maven-plugin 插件,指定基础测试类(下一节会创建):

<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>2.1.1.RELEASE</version>
    <extensions>true</extensions>
    <configuration>
        <baseClassForTests>
            com.baeldung.spring.cloud.springcloudcontractproducer.BaseTestClass
        </baseClassForTests>
    </configuration>
</plugin>

2.2. 生产者端配置

在测试包中创建基础测试类,用于加载 Spring 上下文:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@DirtiesContext
@AutoConfigureMessageVerifier
public class BaseTestClass {

    @Autowired
    private EvenOddController evenOddController;

    @Before
    public void setup() {
        StandaloneMockMvcBuilder standaloneMockMvcBuilder 
          = MockMvcBuilders.standaloneSetup(evenOddController);
        RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);
    }
}

/src/test/resources/contracts/ 目录下定义契约测试桩,例如 shouldReturnEvenWhenRequestParamIsEven.groovy

import org.springframework.cloud.contract.spec.Contract
Contract.make {
    description "should return even when number input is even"
    request{
        method GET()
        url("/validate/prime-number") {
            queryParameters {
                parameter("number", "2")
            }
        }
    }
    response {
        body("Even")
        status 200
    }
}

执行构建时,插件会自动生成名为 ContractVerifierTest 的测试类(继承自 BaseTestClass),存放在 /target/generated-test-sources/contracts/ 目录。

✅ 测试方法命名规则:validate_ + Groovy 文件名(如 validate_shouldReturnEvenWhenRequestParamIsEven

自动生成的测试类示例:

public class ContractVerifierTest extends BaseTestClass {

@Test
public void validate_shouldReturnEvenWhenRequestParamIsEven() throws Exception {
    // given:
    MockMvcRequestSpecification request = given();

    // when:
    ResponseOptions response = given().spec(request)
      .queryParam("number","2")
      .get("/validate/prime-number");

    // then:
    assertThat(response.statusCode()).isEqualTo(200);
    
    // and:
    String responseBody = response.getBody().asString();
    assertThat(responseBody).isEqualTo("Even");
}

构建过程还会将桩文件 JAR 安装到本地 Maven 仓库,供消费者端使用。桩文件位于 stubs/mapping/ 目录。

3. 消费者端(客户端)

消费者端通过 HTTP 交互使用生产者生成的桩文件来维护契约一致性。⚠️ 任何生产者端的变更都可能导致契约失效。

创建 BasicMathController 发起 HTTP 请求获取桩文件响应:

@RestController
public class BasicMathController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/calculate")
    public String checkOddAndEven(@RequestParam("number") Integer number) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add("Content-Type", "application/json");

        ResponseEntity<String> responseEntity = restTemplate.exchange(
          "http://localhost:8090/validate/prime-number?number=" + number,
          HttpMethod.GET,
          new HttpEntity<>(httpHeaders),
          String.class);

        return responseEntity.getBody();
    }
}

3.1. Maven 依赖

消费者端需要添加以下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-wiremock</artifactId>
    <version>2.1.1.RELEASE</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-stub-runner</artifactId>
    <version>2.1.1.RELEASE</version>
    <scope>test</scope>
</dependency>

3.2. 消费者端配置

配置桩运行器(Stub Runner),使其识别本地 Maven 仓库中的桩文件:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@AutoConfigureStubRunner(
  stubsMode = StubRunnerProperties.StubsMode.LOCAL,
  ids = "com.baeldung.spring.cloud:spring-cloud-contract-producer:+:stubs:8090")
public class BasicMathControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void given_WhenPassEvenNumberInQueryParam_ThenReturnEven()
      throws Exception {
 
        mockMvc.perform(MockMvcRequestBuilders.get("/calculate?number=2")
          .contentType(MediaType.APPLICATION_JSON))
          .andExpect(status().isOk())
          .andExpect(content().string("Even"));
    }
}

@AutoConfigureStubRunnerids 属性配置说明:

  • com.baeldung.spring.cloud:生产者 JAR 的 groupId
  • spring-cloud-contract-producer:生产者 JAR 的 artifactId
  • 8090:桩文件运行端口

4. 契约被破坏的场景

如果生产者端修改了影响契约的代码(如接口路径),但未同步更新消费者端,会导致契约测试失败

🚨 示例踩坑:将生产者端接口路径从 /validate/prime-number 改为 /validate/change/prime-number,消费者端未更新时:

  • 消费者仍请求旧路径 /validate/prime-number
  • 抛出异常:org.springframework.web.client.HttpClientErrorException: 404 Not Found

5. 总结

Spring Cloud Contract 通过以下机制保障服务契约一致性:

  • ✅ 自动生成基于契约的测试用例
  • ✅ 桩文件隔离生产者与消费者测试
  • ✅ 早期发现接口变更导致的契约破坏

完整实现代码可参考 GitHub 仓库


原始标题:An Intro to Spring Cloud Contract