In this post, we will explore an introduction to Spring Cloud Contract, which was created to assist in implementing Consumer-Driven Contracts (CDC). That is, ensuring a contract between a Producer and a consumer in a distributed system through messages or HTTP requests.
To showcase the main features and the associated example, we will create both a consumer and a producer, which will be linked through a contract.
In our example, we will use Lombok to eliminate boilerplate code and simplify the implementation.
Objective of Spring Cloud Contract
As previously mentioned, Spring Cloud Contract was created with the aim of implementing and validating Consumer-Driven Contracts, a pattern in which the consumer captures its expectations provided by a provider in a contract. To validate these contracts and know when they can be broken, we make use of Spring Cloud Contract. Among the main features are:
- Ensuring that messages or HTTP requests do exactly what has been implemented on the producer or server side.
- Creating acceptance tests for our services.
- Providing a way to publish changes in contracts that are immediately visible on both sides of communication.
- Generating test code based on the contracts made on the server.
Example of Spring Cloud Contract
At this point, we will create two projects to make use of Spring Cloud Contract. These projects will utilize JUnit 5 to perform contract verification.
The example will consist of a consumer and a producer. The consumer, through a web client, will make a request to the producer, and these calls will be the ones that Spring Cloud Contract will verify do not change. Our producer will consist of two very simple endpoints, /cars and /cars/{id}
The complete example can be found on our github.
Producer
The first piece we’re going to create is our producer, which will have two basic endpoints that will receive HTTP requests. The producer will have an in-memory H2 database to store cars. The structure will be the classic controller-service-repository; we will now show the three layers.
Maven dependencies
The dependencies we need for our producer include the following:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-verifier</artifactId> <scope>test</scope> </dependency>
and we should also configure our 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.refactorizando.sample.springcloudcontract.producer.BaseTestClass </baseClassForTests> </configuration> </plugin>
Creating a Controller for the Producer
Below, we present our controller for our Producer, which consists of two basic endpoints that will make calls to a CarService.
@RestController @RequiredArgsConstructor @RequestMapping("/cars") public class CarController { private final CarService carService; @GetMapping public ResponseEntity<List<Car>> getCars() { var car = carService.findAll(); return new ResponseEntity<>(car, HttpStatus.OK); } @GetMapping("/{id}") public ResponseEntity<Car> getCarById(@PathVariable Long id) { var car = carService.findById(id); return new ResponseEntity<>(car, HttpStatus.OK); } }
Creation of the Producer Service
The service will be an intermediate layer to call the repository:
@RequiredArgsConstructor @Service public class CarService { private final CarRepository carRepository; public Car findById(Long id) { return carRepository.findById(id).orElseThrow(); } public List<Car> findAll() { return carRepository.findAll(); } }
Creation of the Producer Repository
This is the database communication layer, for which we use JPA:
public interface CarRepository extends JpaRepository<Car, Long> { Optional<Car> findByColor(String color); }
Creation of Tests in Producer
Once we have created our application, we will start working with Spring Cloud Contract. To do this, we will add our contracts, whether for REST or messaging, in src/test/resources/contracts. This path is the default and is defined in contractsDslDir. These files can be expressed in Groovy DSL or YAML.
Our contracts will need to verify the /cars and /cars/{id} endpoints that we have defined in our controller. In our case, we have created them with Groovy and will perform a GET request to these endpoints to verify that the call has been made correctly.
package contracts import org.springframework.cloud.contract.spec.Contract Contract.make { description 'Should return one car in a list' request { method GET() url '/cars' } response { status OK() body '''\ [ { "id": 1, "color": "yellow", "model": "Mustang", "brand": "Ford" } ] ''' headers { contentType('application/json') } } }
After adding the contracts, we need to add our BaseTestClass to the path defined in our pom.xml, specifically in the spring-cloud-contract-maven-plugin plugin:
@ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) @DirtiesContext @AutoConfigureMessageVerifier public class BaseTestClass { @Autowired private CarController carController; @BeforeEach public void setup() { StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(carController); RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder); } }
Remember to place @AutoConfigureMessageVerifier on the base class of your generated tests. Otherwise, the messaging part of Spring Cloud Contract Verifier won’t work.
When we run:
mvn clean install
The configured Maven plugin will generate a test class titled ContractVerifierTest that extends from our base class and place it in /target/generated-test-sources/contracts. In this class, you will see the creation of tests that have been added within the resources folder. Note that the path to the contracts must be well defined in our pom.xml.
public class ContractVerifierTest extends BaseTestClass { @Test public void validate_shouldReturnCars() throws Exception { // given: MockMvcRequestSpecification request = given(); // when: ResponseOptions response = given().spec(request) .get("/cars"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).matches("application/json.*"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).array().contains("['id']").isEqualTo(1); assertThatJson(parsedJson).array().contains("['color']").isEqualTo("yellow"); assertThatJson(parsedJson).array().contains("['model']").isEqualTo("Mustang"); assertThatJson(parsedJson).array().contains("['brand']").isEqualTo("Ford"); } @Test public void validate_shouldReturnOneCar() throws Exception { // given: MockMvcRequestSpecification request = given(); // when: ResponseOptions response = given().spec(request) .get("/cars/id"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).matches("application/json.*"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).field("['id']").isEqualTo(1); assertThatJson(parsedJson).field("['color']").isEqualTo("yellow"); assertThatJson(parsedJson).field("['model']").isEqualTo("Mustang"); assertThatJson(parsedJson).field("['brand']").isEqualTo("Ford"); } }
Additionally, within the target folder, the Wiremock stub will have been generated with the name spring-cloud-contract-producer-0.0.1-SNAPSHOT-stubs.jar. This JAR can be made available and uploaded to Maven Central or Artifactory to be verified during the execution of our CI cycle for the validation of our services.
Consumer or client
The client or consumer side will be responsible for consuming the stubs generated by the producer, so any change in the producer will result in a contract breakage.
In our example, we will create a controller that, through a WebClient, will invoke the producer service we have created.
To perform the interaction test with the external service, in this case, with the consumer, WireMock will be used in the test to simulate the call.
Consumer Maven Dependencies
For the consumer side, we will add spring-cloud-contract-stub-runner instead of spring-cloud-contract-verifier and spring-cloud-contract-wiremock, in order to simulate the call to the external service. There is no need to add the previously generated stub.jar to use it in our example.
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-wiremock</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-contract-stub-runner</artifactId> <scope>test</scope> </dependency>
Client controller
Our controller on the client side will have two endpoints that it will handle by invoking, through a WebClient, the producer or consumer service.
@RestController public class CarController { private WebClient webClient; @PostConstruct public void setUpWebClient() { var httpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2_000) .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(2)) .addHandlerLast(new WriteTimeoutHandler(2))); this.webClient = WebClient.builder() .baseUrl("http://localhost:8090") .clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); } @GetMapping("/cars") public ResponseEntity<List<Car>> getCars() { var cars = webClient.get().uri("/cars").accept(MediaType.APPLICATION_JSON) .retrieve().bodyToMono(Car[].class).block(); return new ResponseEntity<>(Arrays.stream(cars).collect(Collectors.toList()), HttpStatus.OK); } @GetMapping("/cars/{id}") public ResponseEntity<Car> getCarById(@PathVariable Long id) { var car = webClient.get().uri("/cars").retrieve().bodyToMono(Car.class).block(); return new ResponseEntity<>(car, HttpStatus.OK); } }
Creating Tests with Spring Cloud Contract in Consumer
Next, we will see how the creation of tests on the consumer side would be using Spring Cloud Contract. For this, what we need is a test that makes use of the stub.jar that has been generated by our producer.
@ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.LOCAL, ids = "com.refactorizando.sample:spring-cloud-contract-producer:+:stubs:8080") public class CarControllerIntegrationTest { @Autowired private CarController carController; @Test void testContractToCarsProducer() { var result = carController.getCars().getBody(); assertTrue(1 == result.size()); var car = result.get(0); assertTrue(car.getBrand().equalsIgnoreCase("Ford")); assertTrue(car.getId().equals(1L)); assertTrue(car.getColor().equalsIgnoreCase("Yellow")); assertTrue(car.getModel().equalsIgnoreCase("Mustang")); } @Test void testContractToCarByIdProducer() { var result = carController.getCarById(1L).getBody(); assertTrue(1 == result.getId()); assertTrue(result.getBrand().equalsIgnoreCase("Ford")); assertTrue(result.getId().equals(1L)); assertTrue(result.getColor().equalsIgnoreCase("Yellow")); assertTrue(result.getModel().equalsIgnoreCase("Mustang")); } }
Next, we will explain what each component means within @AutoConfigureStubRunner.
- com.refactorizando.sample — is the groupId of the artifact defined in the groupId of our artifact.
- spring-cloud-contract-producer — is the artifactId of the producer of the stub.jar.
- 8080 — is the port on which the generated stub will run.
This consumer will use WireMock to simulate the call to the producer. Once the execution is complete, you can see if the tests have returned the expected result from the contract execution.
In this case, we have verified the contract locally using the stub.jar in our .m2 folder. However, for instance, in a CI cycle, it could be stored remotely to execute these tests.
Conclusion
In this post, we have seen an introduction and example of Spring Cloud Contract, which will help us maintain contracts between producer and consumer. This will be particularly beneficial in a microservices architecture, ensuring that all involved services work cohesively with each other.
If you want, you can check the complete example on our GitHub.