Intro to Spring Cloud Contract

Everyone is into micro services now a days. Deploying new services into today’s distributed system requires another level of confidence. Spring cloud contract provides support for Consumer Driven Contracts (CDC) and service schemas in Spring applications. It enables Consumer Driven Contract (CDC) development of spring applications and makes sure that the new feature(i.e. API) that we are going to add in our existing application, should be easily consumed by some other consumer.

CDC Testing approach is nothing but a contract between the producer and the consumer.

How It Works

Spring cloud contract test steps can be divided into two parts

Producer Side: –

Build one Hello World Spring Boot application using https://start.spring.io Once you have that ready add below dependency in your pom.

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

And we’ll need to configure spring-cloud-contract-maven-plugin that will generate tests and stubs for you, with the name of our base test class like below

<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.tektutorial.spring.cloud.contract.TekturialSctProducerApplicationTests
      </baseClassForTests>
   </configuration>
</plugin>

We need to add a base class test in the test package. It could look like…

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

   @Before
   public void setup() {
      RestAssuredMockMvc.standaloneSetup(new FraudDetectionController());
   }
}

In the src/test/resources/contracts add few contract definition(s).
I have written for two scenarios.

package contracts
[
org.springframework.cloud.contract.spec.Contract.make {
    name("Fraud Test")
    request {
        method 'GET'
        url '/fraudcheck?loanAmount=99999'
    }
    response {
        status 200
        body("""{
    "fraudCheckStatus": "FRAUD",
    "rejectionReason": "Amount too high"
  }""")
        headers {
            header('Content-Type': 'text/plain;charset=ISO-8859-1')
        }
    }
},

org.springframework.cloud.contract.spec.Contract.make {
    name("Non-Fraud Test")
    request {
        method 'GET'
        url '/fraudcheck?loanAmount=9999'
    }
    response {
        status 200
        body("""{
    "fraudCheckStatus": "Not-Fraud",
    "rejectionReason": "Amount OK"
  }""")
        headers {
            header('Content-Type': 'text/plain;charset=ISO-8859-1')
        }
    }
}
]

Once you try to build your application from your contracts, tests will be generated in the output folder under /generated-test-sources/contracts. And it will look like below.

package com.tektutorial.spring.cloud.contract;

//imports

public class ContractVerifierTest extends TekturialSctProducerApplicationTests {

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

      // when:
         ResponseOptions response = given().spec(request)
               .get("/fraudcheck?loanAmount=99999");

      // then:
         assertThat(response.statusCode()).isEqualTo(200);
         assertThat(response.header("Content-Type")).isEqualTo("text/plain;charset=ISO-8859-1");
      // and:
         String responseBody = response.getBody().asString();
         assertThat(responseBody).isEqualTo("{\n    \"fraudCheckStatus\": \"FRAUD\",\n    \"rejectionReason\": \"Amount too high\"\n  }");
   }

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

      // when:
         ResponseOptions response = given().spec(request)
               .get("/fraudcheck?loanAmount=9999");

      // then:
         assertThat(response.statusCode()).isEqualTo(200);
         assertThat(response.header("Content-Type")).isEqualTo("text/plain;charset=ISO-8859-1");
      // and:
         String responseBody = response.getBody().asString();
         assertThat(responseBody).isEqualTo("{\n    \"fraudCheckStatus\": \"Not-Fraud\",\n    \"rejectionReason\": \"Amount OK\"\n  }");
   }
}

Once you make them pass and re-run the build and installation of your artifacts then Spring Cloud Contract Verifier will convert the contracts into an HTTP server stub definitions. The stub will be present in the output folder under stubs/../../mappings/ and will look like this:

{
  "id" : "ba9922ed-3ff0-46f8-a293-fcd76c32b23a",
  "request" : {
    "url" : "/fraudcheck?loanAmount=99999",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}",
    "headers" : {
      "Content-Type" : "text/plain;charset=ISO-8859-1"
    },
    "transformers" : [ "response-template" ]
  },
  "uuid" : "ba9922ed-3ff0-46f8-a293-fcd76c32b23a"
}

{
  "id" : "393db092-210c-430d-b0bf-39913c1598e5",
  "request" : {
    "url" : "/fraudcheck?loanAmount=9999",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "{\"fraudCheckStatus\":\"Not-Fraud\",\"rejectionReason\":\"Amount OK\"}",
    "headers" : {
      "Content-Type" : "text/plain;charset=ISO-8859-1"
    },
    "transformers" : [ "response-template" ]
  },
  "uuid" : "393db092-210c-430d-b0bf-39913c1598e5"
}

Consumer Side: –

For Consumer side as well better you keep one simple Spring Boot application ready which will behave like a consumer of all the exposed APIs in Producer. Lets assume you have that ready.

Add below dependencies in your pom.xml

<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>

The last step is to setup Stub Runner in your tests to automatically download the required stubs. Which will help Consumer to detect the changes in Producer. To achieve that you have to pass the  @AutoConfigureStubRunner annotation.

package com.tektutorial.spring.cloud.contract;

//imports

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureStubRunner(
      ids = {"com.tektutorial.spring.cloud.contract:tekturial-sct-producer:+:stubs:8100"},
      stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class TektutorialSctConsumerApplicationTests {
   
   @Test
   public void test()
         throws Exception {

      RestTemplate restTemplate = new RestTemplate();

      ResponseEntity<String> personResponseEntity =
            restTemplate.getForEntity("http://localhost:8100/fraudcheck?loanAmount=99999", String.class);

      ObjectMapper mapper = new ObjectMapper();
      Map<String, String> map = mapper.readValue(personResponseEntity.getBody(), Map.class);
      Assert.assertEquals(map.get("fraudCheckStatus"), "FRAUD");
   }

   @Test
   public void testPositive()
         throws Exception {

      RestTemplate restTemplate = new RestTemplate();

      ResponseEntity<String> personResponseEntity =
            restTemplate.getForEntity("http://localhost:8100/fraudcheck?loanAmount=9999", String.class);

      ObjectMapper mapper = new ObjectMapper();
      Map<String, String> map = mapper.readValue(personResponseEntity.getBody(), Map.class);
      Assert.assertEquals(map.get("fraudCheckStatus"), "Not-Fraud");

   }
}

The @AutoConfigureStubRunner helping us to resolve the stub. Its saying, an artifact with group id com.tektutorial.spring.cloud.contract, artifact id tekturial-sct-producer, in latest version, with stubs classifier will be registered at port 8100

stubsMode is local mean. The stub will be pulled from local maven repository

Once Consumer side test context got booted up, executing the following code will not lead to a 404 because the Spring Cloud Contract Stub Runner will automatically start a WireMock server inside your test and feed it with the stubs generated from the server side.

When it will break

So it will break if you change your Producer side APIs response without updating any Consumer side. i.e. As you can see above in Consumer side test case we have written

Assert.assertEquals(map.get("fraudCheckStatus"), "FRAUD");

If in Producer side we remove or change fraudCheckStatus to something else then your consumer side test will start breaking.

Whats its not: –

Spring Cloud Contract Verifier’s purpose is NOT to start writing business features in the contracts.

Purposes

The main purposes of Spring Cloud Contract Verifier with Stub Runner are:

  • To ensure that Messaging stubs do exactly what the actual server-side implementation does.
  • To promote ATDD method and Microservices architectural style.
  • To provide a way to publish changes in contracts that are immediately visible on both sides.
  • To generate boilerplate test code to be used on the server side.

Hope this must have cleared some of your doubts about Spring Cloud Contract Tests.

The above code can be found on GitHub repos Consumer and Producer