1. The Problem

A common problem often arises with microservices: clients can break in the event that the implementation of a service’s API changes. Endpoints can change, representations of information received over the wire can change.

Our stub is at the moment a correct representation of the fortune service. But as soon as the fortune service’s API changes, this stub will stop being a reflection of reality. And our tests are in danger of continuing to pass, even though in production our client’s calls would fail against the true, newly-updated backing service.

In other words, we have a vulnerability: we are not alerted to a breaking change in the contract.

This is one of the main problems that consumer-driven contract libraries are here to solve: is there a way, as soon as a contract changes, to alert clients to this fact?

Let’s explore the Spring Cloud Contract project.

2. Spring Cloud Contract

The main ideas in the Spring Cloud Contract project are as follows:

  1. Encode a service’s API in a groovy dsl, known as the Contract Definition Language

  2. Generate tests that verify that the implementation of the service adheres to the contract

  3. Generate stubs that adhere to and reflect the contract, so that clients always test against an up-to-date representation of the contract

2.1. Fortune Service Contract

In this simple scenario, the contract can be summed up as follows: the fortune service should expose a single endpoint (/) that can be accessed with an HTTP GET, and that will return a json object with a single property named fortune whose value is the fortune itself, a phrase.

Here’s a valid spring cloud contract that represents the above:

description "The fortune service API"
request {
  method 'GET'
  url '/'
}
response {
  status 200
  headers {
    contentType(applicationJson())
  }
  body """
{ "fortune": "a random fortune" }
"""
}

The above contract hard-codes a value for the fortune. It doesn’t have to, it could instead specify that the value is non-blank, like so:

body """
{ "fortune": ${nonBlank()} }
"""

The above is an example of a groovy domain-specific language. nonBlank() is a built-in function that resolves to a regular expression that ensures the value of fortune is not blank. Groovy is in some ways ideal for the job of expressing contracts, for these reasons:

  1. It’s easy to develop DSL’s in groovy

  2. Support for command-completion in IDE’s

  3. Features such as heredocs in groovy make it easy to encode json without having to deal with escaping double quotes; they also support the interpolation of variables and expressions.

2.1.1. Where to place the contract?

By default, Spring Cloud Contract expects to find contracts in a service’s test resources folder, in a subdirectory named contracts. For each contract, create a subdirectory within contracts, in this case we’ll name it fortunes. The groovy file can have any name we want, let’s call it fortunesContract.groovy. Here’s the complete implementation, including the package declaration and import statement:

src/test/resources/contracts/fortunes/fortuneContract.groovy
package fortunes

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  description "The fortune service API"
  request {
    method 'GET'
    url '/'
  }
  response {
    status 200
    headers {
      contentType(applicationJson())
    }
    body """
{ "fortune": "a random fortune" }
"""
  }
}

Be sure to place the above file in the fortune service, under src/test/resources/contracts/fortunes.

2.1.2. Dependencies

For this contract to be properly interpreted, we need to update our build file to include the necessary dependencies. Below we show in diff form, what additions need to be made to the fortune-service’s build.gradle file in order to add this dependency:

fortune-service/build.gradle
@@ -1,6 +1,7 @@
 buildscript {
   ext {
     springBootVersion = '1.5.10.RELEASE'
+    contractVerifierVersion = '1.2.3.RELEASE'
   }
   repositories {
     mavenCentral()
@@ -21,6 +22,11 @@
   mavenCentral()
 }

+dependencyManagement {
+  imports {
+    mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${contractVerifierVersion}"
+  }
+}

 dependencies {
   compile 'org.springframework.boot:spring-boot-starter-web'
@@ -31,4 +37,5 @@
   compileOnly 'org.projectlombok:lombok'

   testCompile 'org.springframework.boot:spring-boot-starter-test'
+  testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
 }

2.2. Test Generation

Another component of Spring Cloud Contract is a build plugin that adds tasks to generate contract tests.

Modify the build file once more, to apply that gradle plugin to our build, and to configure it:

fortune-service/build.gradle
@@ -8,9 +8,12 @@
   }
   dependencies {
     classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+    classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${contractVerifierVersion}"
   }
 }

+apply plugin: 'spring-cloud-contract'
+
 apply plugin: 'java'
 apply plugin: 'eclipse'
 apply plugin: 'org.springframework.boot'
@@ -28,6 +31,11 @@
   }
 }

+contracts {
+  packageWithBaseClasses = 'io.pivotal.training.fortune' (1)
+  basePackageForTests = 'io.pivotal.training.fortune' (2)
+}
+
 dependencies {
   compile 'org.springframework.boot:spring-boot-starter-web'
   compile 'org.springframework.boot:spring-boot-starter-actuator'
1 generate tests in the specified java package
2 tests will extend a base class, and base class can be found in the specified java package

With these changes in place, we now have additional gradle tasks:

$ gradle tasks

...
Verification tasks
------------------
check - Runs all checks.
copyContracts - Copies contracts to the output folder
generateClientStubs - Generate client stubs from the contracts
generateContractTests - Generate server tests from the contracts
generateWireMockClientStubs - DEPRECATED: Generate WireMock client stubs from the contracts. Use generateClientStubs task.
test - Runs the unit tests.
verifierStubsJar - Creates the stubs JAR task

In particular, note the generateContractTests target. Go ahead and invoke it.

$ gradle generateContractTests

By default tests are written to this directory: build/generated-test-sources/contracts. Here is what the generated test looks like:

package io.pivotal.training.fortune;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import io.restassured.response.ResponseOptions;
import io.pivotal.training.fortune.FortunesBase;
import org.junit.Test;

import static io.restassured.module.mockmvc.RestAssuredMockMvc.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;

public class FortunesTest extends FortunesBase {

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

		// when:
			ResponseOptions response = given().spec(request)
					.get("/");

		// 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("['fortune']").isEqualTo("a random fortune");
	}

}

It’s basically a controller test, just like the one we wrote in the previous section, the main difference being that this test uses a popular testing library named RestAssured.

For these tests to run, we need to write the base class. Here, Spring Cloud Contract employs a convention: it will look for a class named FortunesBase.java, the prefix (Fortunes) has to match the name of the directory that the contract DSL is located in.

Here’s an implementation that will suit us:

src/test/java/io/pivotal/training/fortune/FortunesBase.java
package io.pivotal.training.fortune;

import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;

import static org.mockito.Mockito.when;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.MOCK;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = MOCK)
public abstract class FortunesBase {

  @MockBean private FortuneService fortuneService;
  @Autowired private FortuneController fortuneController;

  @Before
  public void setup() {
    when(fortuneService.getFortune()).thenReturn("a random fortune");

    RestAssuredMockMvc.standaloneSetup(fortuneController);
  }
}

This should look familiar, it’s similar to the way the class FortuneControllerTests is configured. Here we mock the fortune service to make it return the same value we encoded in the contract.

Now we’re ready to run our tests:

$ gradle test

The tests should pass. Check the gradle test report located at build/reports/tests/test/index.html.

verifier test result

At this point, the test we wrote in the last section, in FortuneControllerTests, is now essentially a duplicate of what Spring Cloud has generated for us. We’re free to delete it.

2.3. Generating the Stub

Spring Cloud Contract has one more trick up its sleeve: it can auto-generate and publish a wiremock stub to a maven repository alongside the service’s main artifact. To do that, Spring Cloud Contract uses a maven classifier named 'stubs' to identify this extra artifact.

Here we will publish our service to our local maven repository. gradle makes this particularly simple. Let’s give our fortune-service the maven groupId io.pivotal.training.springcloud and apply the gradle maven-publish plugin:

@@ -12,10 +12,7 @@ buildscript {
   }
 }

-group = 'io.pivotal.training.fortune'
+group = 'io.pivotal.training.springcloud'
+
 apply plugin: 'spring-cloud-contract'
+apply plugin: 'maven-publish'

 apply plugin: 'java'
 apply plugin: 'eclipse'

Now we’re ready to publish our project (including the stub) to maven local:

$ gradle publishToMavenLocal

2.4. The Stub Runner

Finally, let’s turn our attention to the greeting application.

Spring Cloud Contract provides a stub runner, whose dependency needs to be added to our project:

@@ -21,6 +21,9 @@
   mavenCentral()
 }

+ext {
+  springCloudVersion = 'Edgware.SR2'
+}

 dependencies {
   compile 'org.springframework.boot:spring-boot-starter-web'
@@ -32,6 +35,12 @@
   compileOnly 'org.projectlombok:lombok'

   testCompile 'org.springframework.boot:spring-boot-starter-test'
-  testCompile 'com.github.tomakehurst:wiremock-standalone:2.14.0' (1)
+  testCompile 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
+}

+dependencyManagement {
+  imports {
+    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
+  }
 }
1 Wiremock becomes a transitive dependency of the runner, we don’t need to cite it explicitly anymore.

Next, with a single annotation, we can replace our hand-coded wiremock stub with the fortune-service’s auto-generate stub, the one that is a reflection of our contract, and that should remain in synch with that contract:

@@ -1,42 +1,23 @@
 package io.pivotal.training.greeting;

-import com.github.tomakehurst.wiremock.client.WireMock;
-import com.github.tomakehurst.wiremock.junit.WireMockRule;
-import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 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.test.context.junit4.SpringRunner;

-import static com.github.tomakehurst.wiremock.client.WireMock.*;
-import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE;

 @RunWith(SpringRunner.class)
 @SpringBootTest(webEnvironment = NONE)
+@AutoConfigureStubRunner(workOffline = true, ids = "io.pivotal.training.springcloud:fortune-service:+:stubs:8081") (1)
 public class FortuneServiceClientTests {

   @Autowired private FortuneServiceClient fortuneServiceClient;

-  @Rule (2)
-  public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().port(8081));
-
-  private static final String ExpectedFortune = "some fortune";
-
-  @Before
-  public void setup() {
-    stubFor(WireMock.get(urlEqualTo("/"))
-        .willReturn(aResponse()
-            .withStatus(200)
-            .withHeader("Content-Type", "application/json")
-            .withBody(String.format("{ \"fortune\": \"%s\" }", ExpectedFortune))
-        ));
-  }
+  private static final String ExpectedFortune = "a random fortune";

   @Test
-  public void shouldReturnAFortune() {
-    assertThat(fortuneServiceClient.getFortune()).isNotBlank();
+  public void shouldReturnAFortuneMatchingContract() {
+    assertThat(fortuneServiceClient.getFortune()).isEqualTo(ExpectedFortune);
   }
1 workOffline means "fetch dependencies from maven local", ids tells where in the repository to find the stub, using the 'stubs' classifier and specifying a port number to run our stub on (8081)
2 Removing all the wiremock-related code and replacing it with the AutoConfigureStubRunner annotation

This test should now pass.

3. A "what-if" Scenario

The big question now is: will our client tests alert us (i.e. fail) if the developers of the fortune service change the contract without notifying us of the breaking change?

Let’s test that:

Pretend that the fortune service developers chose to rename the field in the json from 'fortune' to 'message'. Edit fortunesContract.groovy such that the body looks like this:

body """
{ "message": "a random fortune" }
"""
}

The first thing we will encounter is a failing contract implementation on the fortune side.

$ gradle clean test

...
java.lang.IllegalStateException: Parsed JSON [{"fortune":"a random fortune"}] doesn't match the JSON path [$[?(@.['message'] == 'a random fortune')]]

Retrofit the implementation of FortuneController to put the fortune using the key message. And otherwise make the fortune tests pass.

Now, let’s publish our updated artifacts to maven:

$ gradle publishToMavenLocal

And finally, let’s do a clean run of the greeting application’s tests. The FortuneServiceClientTests should fail.

Client Test Failure

That’s the role of contract tests: keeping both sides of the contract in sync with one another, and alerting clients early to breaking changes in the contract.

➜ In preparation for the next lab, undo the above changes associated with the what-if scenario: set the key name in the body of the json repsonse back to fortune, and re-publish to mavenLocal.

4. Congralutations

We’ve covered a lot of ground; I hope this lab gave you a taste of how Spring Cloud Contract works. If you’re curious to learn more, head on over to the project’s reference guide.

Next, let’s talk about failures.