Spring Cloud Contract

Dominik Adamek

Spring Cloud Contract helps developers in implementing the Consumer Driver Contracts(CDC) approach. In this article, we’ll explain what is Consumer Driven Contract Testing and see how to use Spring Cloud Contract project to implement CDC. As an example, we’ll use two Spring Boot applications: a Producer exposing a REST API, and a Consumer being a client of Producer’s API.

Consumer Driver Contracts

Let’s start from a bit of theory. Spring Cloud Contract is a quite fresh project (released on 2016), however, it’s core concept, the CDC pattern, has been present in the IT world for quite a long time. Simply put, CDC inverts the ownership of the contract between a Producer and a Consumer, where the Consumer side defines the contract and expects Producer to fulfill it. If we consider CDC in the context of HTTP/message based interactions between Microservices, the Consumer side requests any REST API changes or messaging changes at the Producer side. If we think about that, it would make perfect sense, as the Consumer is the only one who is using the Producer’s API, so giving him the control over contract changes sounds very reasonable.

Typical pitfalls of Microservice integration testing

If it comes to verifying communication between Microservices, many developers start from creating end-to-end tests. This approach will be fine enough for testing a system, where the number of Microservices is relatively small. However, for a more complex systems, with tens of Microservices, databases etc., setting it up might be a nightmare. It may also take a long time until you receive a feedback after such end-to-end test run. You may overcome such issues if you decide to convert a heavy end-to-end test, to an integration test with mocks instead of real services. Obviously your test setup won’t be such prod-like as in end-to-end test, but you will receive a fast feedback without a need to set up the whole system. However, introducing mocks may also come with some disadvantages. We need to keep in mind that mock is just a mock, and there’s still a risk that you can go live with all builds passing but system may crush on production. Simply put, we often implement mocks on a Consumer side, with no guarantee that it works just the same as real system.

Spring Cloud Contract Verifier

Spring Cloud Contract Verifier is a tool that comes along with Spring Cloud Contract. It offers creating Contract definitions in Groovy or YAML. Contract is an agreement between a Consumer and a Producer, on how the communication should look like. Even though it might look similar to Swagger definition, especially when written in YAML, please keep in mind that Contract is not a schema of an API. This concept is totally different as it’s more like a test scenario, where you expect a specific response on a given request. Apart from creating Contract definitions, Spring Cloud Contract Verifier offers you more features:

  • provides fast feedback
  • makes your mocks/stubs doing exactly what real Producer service does
  • generates tests based on Contract definitions on Producer side
  • Contract changes are visible on both Consumer and Producer side

Producer side setup

Let’s start from setting up Spring Cloud Contract on a Producer side. In our example, we will create a simple REST API and use Spring Cloud Contract to generate API tests based on Contract definitions. You will need:

  • Maven
  • Spring Boot 2.5.5 with spring-boot-starter-webspring-boot-starter-test and spring-boot-starter-validation dependencies
  • Spring Cloud 2020.0.4
  • Lombok
  • Spock 2.0-groovy-3.0

In order to start working with Spring Cloud Contract, you need to update your pom.xml with:

  • Spring Cloud Contract Verifier dependency:
<dependency>
    <groupid>org.springframework.cloud</groupid>
    <artifactid>spring-cloud-starter-contract-verifier</artifactid>
    <scope>test</scope>
</dependency>
  • Spring Cloud Contract Maven plugin:
    <plugin>
       <groupid>org.springframework.cloud</groupid>
       <artifactid>spring-cloud-contract-maven-plugin</artifactid>
       <version>3.0.4</version>
       <executions>
           <execution>
               <goals>
                   <goal>convert</goal>
                   <goal>generateStubs</goal>
                   <goal>generateTests</goal>
               </goals>
           </execution>
       </executions>
       <extensions>true</extensions>
       <configuration>
           <baseclassfortests>org.example.BaseContractVerifier</baseclassfortests>
           <testframework>SPOCK</testframework>
       </configuration>
    </plugin>

    For the purpose of this article, we will use Spock as a testing framework, but feel free to use standard JUnit as well. To explore Spring Cloud Contract features, we will create a very basic Review API. In order to follow TDD approach in context of API design, we will create Contract definitions for our Review API under test/resources/contracts/review directory. However, you can keep your Contracts in a separate Maven repository. Spring Cloud Contract Maven plugin can be easily configured to fetch Contracts from Maven repository instead of Producer’s local resources. This approach is even more consumer-driven as you may want Contracts repository to be managed by Consumer side.

    Let’s create two Contracts using Groovy DSL:

    • should _add _review.groovy:
      package contracts.review
      
      import org.springframework.cloud.contract.spec.Contract
      
      Contract.make {
      
          description 'should add review'
      
          request {
              url "/review"
              method POST()
              headers {
                  contentType applicationJson()
              }
              body(
                  user: 'Joe',
                  comment: 'Amazing driving experience. Great car!',
                  rate: 10
              )
          }
      
          response {
              status OK()
              headers {
                  contentType applicationJson()
              }
              body (
                      id: $(uuid()),
                      user: 'Joe',
                      comment: 'Amazing driving experience. Great car!',
                      rate: 10
              )
          }
      
      }
      • should _reject _review _if _user _not _provided.groovy:
        package contracts.review
        
        import org.springframework.cloud.contract.spec.Contract
        
        Contract.make {
        
            description 'should reject review when request is missing required field: user'
        
            request {
                url "/review"
                method POST()
                headers {
                    contentType applicationJson()
                }
                body(
                        comment: 'Worst car ever. Do not buy it!',
                        rate: 0
                )
            }
        
            response {
                status BAD_REQUEST()
            }
        
        }

        Above examples are just basic Contract definitions. Contract DSL offers you many options to describe your Contract. You can use dynamic properties, define a method call that executes during the test, reference the request from the response using fromRequest() method and many more.

        The baseClassForTests option of Spring Cloud Contract Maven plugin, allows you to specify the base class to be used in generated Contract tests. Let’s create it:

        package org.example
        
        import io.restassured.module.mockmvc.RestAssuredMockMvc
        import org.spockframework.spring.SpringBean
        import org.springframework.beans.factory.annotation.Autowired
        import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
        import org.springframework.web.context.WebApplicationContext
        import spock.lang.Specification
        
        @WebMvcTest
        abstract class BaseContractVerifier extends Specification {
        
            @Autowired
            WebApplicationContext webApplicationContext
        
            def setup() {
                RestAssuredMockMvc.webAppContextSetup(webApplicationContext)
            }
        
        }
        

        Instead of standard @SpringBootTest approach, we will be using @WebMvcTest to avoid setting up the whole Spring’s Context, but rather focus on testing the web layer of our application. Our generated tests will be using RestAssuredMockMvc and the base class seems to be a good place to initialize it with Spring’s WebApplicationContext.

        If you run your Maven build now, you will see the Contract tests generated by Spring Cloud Contract:

        package org.example
        
        import org.example.BaseContractVerifier
        import com.jayway.jsonpath.DocumentContext
        import com.jayway.jsonpath.JsonPath
        import spock.lang.Specification
        import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification
        import io.restassured.response.ResponseOptions
        
        import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
        import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
        import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
        import static io.restassured.module.mockmvc.RestAssuredMockMvc.*
        
        @SuppressWarnings("rawtypes")
        class ReviewSpec extends BaseContractVerifier {
        
         def validate_should_add_review() throws Exception {
          given:
           MockMvcRequestSpecification request = given()
             .header("Content-Type", "application/json")
             .body('''{"user":"Joe","comment":"Amazing driving experience. Great car!","rate":10}''')
        
          when:
           ResponseOptions response = given().spec(request)
             .post("/review")
        
          then:
           response.statusCode() == 200
           response.header("Content-Type") ==~ java.util.regex.Pattern.compile('application/json.*')
        
          and:
           DocumentContext parsedJson = JsonPath.parse(response.body.asString())
           assertThatJson(parsedJson).field("['id']").matches("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")
           assertThatJson(parsedJson).field("['user']").isEqualTo("Joe")
           assertThatJson(parsedJson).field("['comment']").isEqualTo("Amazing driving experience. Great car!")
           assertThatJson(parsedJson).field("['rate']").isEqualTo(10)
         }
        
         def validate_should_reject_review_if_user_not_provided() throws Exception {
          given:
           MockMvcRequestSpecification request = given()
             .header("Content-Type", "application/json")
             .body('''{"comment":"Worst car ever. Do not buy it!","rate":0}''')
        
          when:
           ResponseOptions response = given().spec(request)
             .post("/review")
        
          then:
           response.statusCode() == 400
         }
        
        }

        Obviously the build is failing now as there’s no API implementation yet. Let’s create a very basic API, with a single endpoint to create user’s review:

        package org.example.review;
        
        import lombok.RequiredArgsConstructor;
        import org.springframework.web.bind.annotation.PostMapping;
        import org.springframework.web.bind.annotation.RequestBody;
        import org.springframework.web.bind.annotation.RestController;
        
        import javax.validation.Valid;
        
        import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
        
        @RestController
        @RequiredArgsConstructor
        public class ReviewController {
        
            private final ReviewService reviewService;
        
            @PostMapping(
                    value = "/review",
                    produces = APPLICATION_JSON_VALUE,
                    consumes = APPLICATION_JSON_VALUE
            )
            Review newEmployee(@RequestBody @Valid Review review) {
                return reviewService.addReview(review);
            }
        
        }
        package org.example.review;
        
        import lombok.AllArgsConstructor;
        import lombok.Data;
        import lombok.NoArgsConstructor;
        
        import javax.validation.constraints.NotBlank;
        import java.util.UUID;
        
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        public class Review {
        
            private UUID id;
        
            @NotBlank
            private String user;
        
            private String comment;
        
            private Integer rate;
        
        }

        We can also update our BaseContractVerifier with a mock for ReviewService:

        package org.example
        
        import io.restassured.module.mockmvc.RestAssuredMockMvc
        import org.example.review.Review
        import org.example.review.ReviewService
        import org.spockframework.spring.SpringBean
        import org.springframework.beans.factory.annotation.Autowired
        import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
        import org.springframework.web.context.WebApplicationContext
        import spock.lang.Specification
        
        @WebMvcTest
        abstract class BaseContractVerifier extends Specification {
        
            @Autowired
            WebApplicationContext webApplicationContext
        
            @SpringBean
            ReviewService reviewService = Mock()
        
            def setup() {
                RestAssuredMockMvc.webAppContextSetup(webApplicationContext)
        
                reviewService.addReview(_ as Review) >> { Review review ->
                    new Review(UUID.randomUUID(), review.user, review.comment, review.rate)
                }
            }
        
        }

        This time you may expect a green build. Additionally, you will find an extra stubs jar file under target directory. It represents a Producer side stub which you can use on the Consumer side.

        Consumer side setup

        To prepare a Consumer setup, you need:

        • Maven
        • Spring Boot 2.5.5 with spring-boot-starter-webspring-boot-starter-test dependencies
        • Spring Cloud 2020.0.4
        • Lombok
        • Spock 2.0-groovy-3.0

        We will start from implementing very basic integration with Producer’s Review API:

        package org.example.review;
        
        import lombok.RequiredArgsConstructor;
        import org.springframework.stereotype.Service;
        import org.springframework.web.client.RestTemplate;
        
        @Service
        @RequiredArgsConstructor
        public class ReviewClient {
        
            private static final String REVIEW_PATH = "/review";
            private final RestTemplate reviewServiceRestTemplate;
        
            public Review addReview(final Review review) {
                return reviewServiceRestTemplate.postForObject(REVIEW_PATH, review, Review.class);
            }
        
        }

        In order to test our integration, we will use Spring Cloud Contract Stub Runner to get a WireMock instance of the Producer side. We will need to add Spring Cloud Contract Stub Runner dependency:

        <dependency>
            <groupid>org.springframework.cloud</groupid>
            <artifactid>spring-cloud-contract-stub-runner</artifactid>
            <scope>test</scope>
        </dependency>

        Below example demonstrates how you can implement your integration test using Spring Cloud Contract Stub Runner:

        package org.example.review
        
        import org.springframework.beans.factory.annotation.Autowired
        import org.springframework.boot.test.context.SpringBootTest
        import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
        import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
        import spock.lang.Specification
        
        @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
        @AutoConfigureStubRunner(
                ids = "org.example:consumer-driven-contracts:+:stubs:8081",
                stubsMode = StubRunnerProperties.StubsMode.LOCAL
        )
        class ReviewClientIntegrationTest extends Specification {
        
            @Autowired
            private ReviewClient reviewClient
        
            def 'should add review'() {
                when:
                def result = reviewClient.addReview(Review.from('Joe', 'Amazing driving experience. Great car!', 10))
        
                then:
                result.getId()
                result.getUser() == 'Joe'
                result.getComment() == 'Amazing driving experience. Great car!'
                result.getRate() == 10
            }
        
        }

        @AutoConfigureStubRunner allows you to autoconfigure your Producer stub and StubsMode.LOCAL instructs Spring Cloud Contract Stub Runner to fetch stubs from your local .m2 directory. You may specify one or more stubs to be run in ids property. If you would like to fetch your stubs from a remote location, rather than local one, you can set repositoryRoot property pointing to your remote location. Make sure you are using StubsMode.REMOTE when downloading your stubs from remote repository.

        We could obviously achieve the same stub behaviour if we would implement a WireMock stubbing for a Producer side ourselves at the Consumer side. The power of Spring Cloud Contract tests will be clearly visible once you introduce a change in the API. Let’s get back to the Producer side for a moment and change the type of our rate property of the Review class from Integer to some enum value:

        package org.example.review;
        
        import lombok.AllArgsConstructor;
        import lombok.Data;
        import lombok.NoArgsConstructor;
        
        import java.util.UUID;
        
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        public class Review {
        
            private UUID id;
        
            private String user;
        
            private String comment;
        
            private Rate rate;
        
            public static enum Rate {
                BAD, AVERAGE, GOOD, GREAT
            }
        
        }

        This time, once you rebuild your Producer app and run the integration test at the Consumer side again, you will observe it fails. Catching such Contract changes when implementing mocks at a Consumer side requires good level of communication between developers at both sides. With Spring Cloud Contract, you will catch all Contract changes automatically at both sides.

        Summary

        In this article, we explored some core concepts of Spring Cloud Contract project based on HTTP integration of two Microservices. Spring Cloud Contract should be considered as a great addition to your testing stack, especially if you are maintaining a complex system with multiple Microservices. In addition, Spring Cloud Contract offers you also a great testing support for message-based integrations (like Kafka). Feel free to try Spring Cloud Contract and implement Consumer Driven Contract testing in your project.

        References

        Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

        Skontaktuj się z nami