AdBlock Detected

It looks like you're using an ad-blocker!

Our team work realy hard to produce quality content on this website and we noticed you have ad-blocking enabled. Advertisements and advertising enable us to continue working and provide high-quality content.

Introduction to Spring Cloud Contract

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:

  1. Ensuring that messages or HTTP requests do exactly what has been implemented on the producer or server side.
  2. Creating acceptance tests for our services.
  3. Providing a way to publish changes in contracts that are immediately visible on both sides of communication.
  4. 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}

Introducción y ejemplo a Spring Cloud Contract
Introduction and example of Spring Cloud Contract

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.

Leave a Reply

Your email address will not be published. Required fields are marked *