Introduction
Imagine the following (producer) rest API which sums 2 numbers (check source code here):
@RestController
@RequestMapping(value = "api/addition", produces = MediaType.APPLICATION_JSON_VALUE)
public class AdditionController {
@PostMapping("{op1}/{op2}")
public Mono<ResponseEntity> sum(@PathVariable("op1") Integer op1, @PathVariable("op2") Integer op2) {
return Mono.just(new Result(BigDecimal.valueOf(op1)
.add(BigDecimal.valueOf(op2))))
.map(ResponseEntity::ok);
}
public record Result(BigDecimal value) {}
}
And the following integration test:
@ExtendWith(SpringExtension.class)
@WebFluxTest(controllers = AdditionController.class)
@AutoConfigureWebTestClient(timeout = "20000")
class AdditionControllerTest {
@Autowired
WebTestClient webTestClient;
@Test
void shouldSum() {
webTestClient.post()
.uri("/api/addition/1/2")
.exchange()
.expectStatus().isOk()
.expectBody().jsonPath("$.value").isEqualTo("3");
}
}
Now whenever we change our API this test may break during build time but the main question is:
How about consumers of our API, how can we break them at build time whenever we introduce a breaking change to our API?
Contract tests to rescue!
Defining the contract (on the producer/provider side)
In this post we are going to use Spring cloud contract with kotlin DSL.
Here’s how our initial contract looks like:
contract {
name = "Addition API contract"
request {
url = url("/api/addition/1/2")
method = POST
}
response {
status = OK
headers {
header(CONTENT_TYPE, APPLICATION_JSON)
}
body = body(mapOf(
"value" to 3
))
}
}
It’s placed in src/test/resources/contracts/AdditionApiContract.kts.
After creating the contract we’ll need to add the following dependencies:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-spec-kotlin</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-verifier</artifactId> <scope>test</scope> </dependency>
And also spring-cloud-contract maven plugin is needed so the contract test and it’s assertions can be generated based on the contract we created:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testFramework>JUNIT5</testFramework>
<testMode>EXPLICIT</testMode>
<baseClassForTests>com.github.rmpestano.addition.ContractBase</baseClassForTests>
</configuration>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-kotlin</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
</dependencies>
</plugin>
testMode EXPLICITmeans our contract test will make real http calls. MockMvc can be used if prefered.baseClassForTestsdefines a base class that our contract tests will extend, in this base classe we can customise our test setup, see here.
After that our contract test will be generated during build time, spring-cloud-contract-verifier creates the test in order to verify we (the producer side still) are complying with our own contract:

NOTE: If we change our API and don’t change the contract, ContractVerifierTest will fail in a similar way we would with an integration test. For example, imagine we rename the response field in Addition API from value to result without changing the contract:

It’s important to understand that Spring cloud contract will generate wiremock mappings based on the contract:

And that’s what the client of our API (consumer side) will be using (at build time) in order to verify whether the contract is broken or not.
Creating the client (consumer) API
Now let’s create the client of our Addition API, a Calculator that will sum 2 numbers. First the endpoint:
@RestController
@RequiredArgsConstructor
@RequestMapping("api/calculator")
public class CalculatorController {
private final AdditionService sumService;
@PostMapping("sum/{op1}/{op2}")
Mono<ResponseEntity> sum(@NonNull @PathVariable("op1") Integer op1, @NonNull @PathVariable("op2") Integer op2) {
return sumService.sum(op1, op2)
.map(ResponseEntity::ok);
}
}
The AdditionService which will be responsible for calling the Addition API:
@Service
@RequiredArgsConstructor
public class AdditionService {
@Qualifier("additionClient")
private final WebClient webClient;
public Mono<Result> sum(@NonNull final Integer op1, @NonNull final Integer op2) {
return webClient.post()
.uri(uriBuilder -> uriBuilder
.path("/{op1}/{op2}")
.build(op1, op2))
.retrieve()
.bodyToMono(Result.class);
}
}
@Configuration
public class WebClientConfig {
private static final String SUM_ENDPOINT = "http://localhost:8181/api/addition";
@Bean
@Qualifier("additionClient")
WebClient additionWebClient() {
return WebClient.builder()
.baseUrl(SUM_ENDPOINT)
.build();
}
}
Creating the client contract test
Finally, let’s create the client side contract test:
@ExtendWith(SpringExtension.class)
@AutoConfigureStubRunner(ids = "com.github.rmpestano:addition-api")
class AdditionServiceTest {
@Autowired
private StubFinder stubFinder;
private AdditionService additionService;
@BeforeEach
public void setupClient() {
WebClient webClient = WebClient.builder()
.baseUrl(
"http://localhost:" + stubFinder.findAllRunningStubs()
.getPort("addition-api") + "/api/addition"
).build();
additionService = new AdditionService(webClient);
}
@Test
void shouldSum() {
Result result = additionService.sum(1, 2).block();
assertThat(result).isNotNull()
.extracting(Result::value)
.isEqualTo(BigDecimal.valueOf(3));
}
}
AutoConfigureStubRunner will fetch the wiremock based stubs provided by Addition API (provider). For that we have to define a classpath dependency to the stub in the consumer API:
<dependency> <groupId>com.github.rmpestano</groupId> <artifactId>addition-api</artifactId> <classifier>stubs</classifier> <version>0.0.1-SNAPSHOT</version> <scope>test</scope> <exclusions> <exclusion> <groupId>*</groupId> <artifactId>*</artifactId> </exclusion> </exclusions> </dependency>
In the @BeforeEach hook we’re just configuring the webclient used by AdditionService so it can call the wiremock stub.
We also need to add spring-cloud-stub-runner dependency on the client:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> <scope>test</scope> </dependency>
Finally, we can run our client contract test and it should break whenever the Addition API introduces a breaking change.
Evolving the contract
Now let’s evolve the contract, force a breaking change and see how our client contract test behaves. First, let’s change our Addition API by removing the operators from the path parameters, moving them to the body of the request and also changing the response body to return result instead of value:
@RestController
@RequestMapping(value = "api/addition", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public class AdditionController {
@PostMapping
public Mono<ResponseEntity> sum(@RequestBody Operator operator) {
return Mono.just(new Result(BigDecimal.valueOf(operator.op1)
.add(BigDecimal.valueOf(operator.op2))))
.map(ResponseEntity::ok);
}
public record Result(BigDecimal result) {}
public record Operator(Integer op1, Integer op2) {}
}
Also, the contract needs to be changed accordingly otherwise ContractVerifierTest will fail as we saw earlier:
contract {
name = "Addition API contract"
request {
url = url("/api/addition/")
method = POST
headers {
header(CONTENT_TYPE, APPLICATION_JSON)
}
body = body(mapOf(
"op1" to 1,
"op2" to 2
))
}
response {
status = OK
headers {
header(CONTENT_TYPE, APPLICATION_JSON)
}
body = body(mapOf(
"result" to 3
))
}
}
Now if we build the Calculator API as is, our client contract test will fail as follows:

It’s still calling the old endpoint so let’s fix the path and pass the operators in the body in the AdditionService:
@Service
@RequiredArgsConstructor
public class AdditionService {
@Qualifier("additionClient")
private final WebClient webClient;
public Mono<Result> sum(@NonNull final Integer op1, @NonNull final Integer op2) {
return webClient.post()
.bodyValue(new AdditionRequest(op1, op2))
.retrieve()
.bodyToMono(Result.class);
}
public record AdditionRequest(Integer op1, Integer op2){}
}
And run our client contract test again, then we’ll get the following error:

That means we fixed the endpoint path but now we fail to match the new response body, the client/consumer is still expecting the old field called value but the new contract is returning result. Changing the Result class in the client to return the new field fixes the problem.
Also, it’s interesting to note that we have an integration test in the Calculator API that mocks the AdditionService call which makes it pass while the contract test still fails showing the real benefit of contract tests:

Conclusion
I hope I was able to demonstrate how to use Spring cloud contracts to avoid breaking your APIs at runtime.
The source code used in this post can be found on this repo. I’ve created separated git tags for each version of the API contract, check here.


















