1. Services integration problem

In this article, we will consider an example of microservice application.

This application consists of task-service and report-service.

diagram of microservices

In the task-service, we can create and get tasks. The report-service provides us the statistics of tasks distribution. For creating these statistics, the report-service is using the API has provided by the task-service.

frontend demo

So, we have two services that communicate together. The "contract" is the API which is using between a pair of microservices. In this article, we will consider a tool to maintain the consistency of the contract between microservices.

In terms of the Consumer Driven Contracts, the task-service is a provider of the API, and report-service is a consumer.

If we change a contract on the producer side, our consumer will not be able to get data from the producer:

put some breakable changes in contract

We need to know that one of the services does not match the contract. And we need to know about this as soon as possible. In this article, we will consider how to prevent this situation by using cloud contracts. In order to do it, we will write contract tests and will consider how to deliver a contract to each of the services.

2. Workflow of using a cloud contract:

spring cloud contracts


  1. we write groovy contract files on the producer side (in the task-service)

  2. build task-service, generate and run contract tests (for JUnit5)

  3. install JAR file with the stub of contracts (json request/response schemes of the task-service)

  4. deploy this stub to the binary repository (Artifactory)

  5. write contract tests on the consumer side (in the report-service)

  6. use the stub from the Artifactory in tests of the consumer

3. Provider configuration

Support for JUnit5 is currently unavailable in the stable release of Spring Cloud Contract(2.0.1). But this feature already completed in the master branch and we can use it from the snapshots.

I think it’s possible to use an unstable version for work with JUnit5, because we will use this just in tests, and the spring cloud contract is a project with a good ecosystem and support. In my opinion, an ability to use JUnit5 in tests provides us a lot of benefits and we can close our eyes to work with the version from snapshots.

We need to add the next repository of snapshots:

3.1. Dependencies

<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/libs-snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>

Also, we need to use a snapshot for plugins, because Spring Cloud Contract generates source code with the help of maven plugin:

<pluginRepositories>
    <pluginRepository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/libs-snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </pluginRepository>
</pluginRepositories>

And now, we can add the next dependency:

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

Also, we need to add a dependency management for spring cloud 2.1.0 snapshot:

<dependencyManagement>
    <dependencies>
        <!-- If you're adding this dependency explicitly you have to
               add it *BEFORE* the Release Train BOM-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-dependencies</artifactId>
            <version>2.1.0.BUILD-SNAPSHOT</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

In the plugin section, we need to configure the spring-cloud-contract-maven-plugin. This plugin is responsible for generating a source code of contract tests:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-contract-maven-plugin</artifactId>
            <version>2.1.0.BUILD-SNAPSHOT</version>

            <extensions>true</extensions>
            <configuration>
                <baseClassForTests>com.antkorwin...MvcTest</baseClassForTests> (1)
                <testFramework>JUNIT5</testFramework> (2)
            </configuration>
        </plugin>

        ..

    </plugins>
<build>
1 The reference to the base class for your auto-generated tests, each of the contract tests will be inherited from this class.
2 The setting for Junit5 tests generation.

3.2. BaseClass for contract tests

This class will be used for generating contract verification tests, as a parent class.

@ExtendWith(SpringExtension.class)  (1)
@SpringBootTest
public class MvcTest {

    @Autowired
    private WebApplicationContext context;

    @BeforeEach
    void setup() {
        RestAssuredMockMvc.webAppContextSetup(context); (2)
    }

}
1 of course, we use JUnit5, and ExtendWith instead of RunWith
2 RestAssured initialization with the application context.

3.3. Describe your contracts

All that we need to make manually is a contract declaration of our API. This is a groovy based DSL, where you can describe a request to your API and expected response for it:

package contracts.tasks.write

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

UUID_REGEX = "([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})";

Contract.make {
    description "Create new task"
    request {
        method POST()
        url("/tasks/create") {
            queryParameters {
                parameter("title", value(consumer(regex("[a-z A-Z]+"))))
                parameter("estimate", value(consumer(regex("[0-9]+"))))
            }
        }
    }
    response {
        body(
                id: value(producer(regex("$UUID_REGEX"))),
                title: fromRequest().query("title"),
                estimate: fromRequest().query("estimate")
        )
        headers {
            contentType(applicationJson())
        }
        status HttpStatus.CREATED.value()
    }
}

You can collect groovy files with contracts in packages and put it in the resources, as you can see here:

contract resource folder

A little later we will consider generated tests from this contracts.

In my opinion, the using of groovy is a more intuitive and maintained way of describing a contract than using something like YAML. By the way, Spring Cloud Contract supports both of these variants (groovy and YAML).

3.4. Generate the contract tests and install stubs

So, if you added all the needed dependencies and wrote a contract, then we can build the project:

$ mvn clean install

...

[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ task-service ---
[INFO] Installing task-service-0.0.1-SNAPSHOT.jar to ~\.m2\...
[INFO] Installing pom.xml to ~\.m2\...
[INFO] Installing task-service-0.0.1-SNAPSHOT-stubs.jar to ~\.m2\...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:47 min
[INFO] Finished at: 2018-09-18T11:07:33+10:00
[INFO] Final Memory: 82M/280M
[INFO] ------------------------------------------------------------------------

After a successful build of your project, you get a generated test for the groovy contract in the /target/generated-test-sources/contracts folder, as you can see on the next image:

generated test source cloud contract

Let’s look at these tests:

public class WriteTest extends MvcTest {  (1)

  @Test
  public void validate_create_task() throws Exception {  (2)
    // given:
    MockMvcRequestSpecification request = given();
    // when:
    ResponseOptions response = given().spec(request)  (3)
                                      .queryParam("title","G ")
                                      .queryParam("estimate","42")
                                      .post("/tasks/create");
		// then:
    assertThat(response.statusCode()).isEqualTo(201);
    assertThat(response.header("Content-Type")).matches("application/json.*");
    // and:
    DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());  (4)
    assertThatJson(parsedJson).field("['title']").isEqualTo("G ");
    assertThatJson(parsedJson).field("['estimate']").isEqualTo("42");
    assertThatJson(parsedJson).field("['id']")
      .matches("([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})");
  }

}
1 the generated test extends from our base test class (MvcTest)
2 the name of the test case generated from the name of the file with a contract
3 this request generated from the contract
4 the response will be verified by the contract.

When you put some breaking changes in the contract, these tests help to find the place which you need to change.

Sometimes the code of these tests looks like boilerplate, but it’s not important, because these tests are needed only to ensure the correctness of the contract.

And if you run it, you can check your contract.

When you run mvn clean install these tests will run automatically, after generation stage. Besides that, after generates and runs all tests maven will install binary artifacts in the local folder .m2

Among these artifacts there is one new - an artifact with the stub of contracts: prediction-service-0.0.1-SNAPSHOT-stubs.jar.

If you need the only generation of test sources without run all tests and install artifacts, you can run mvn generate-test-sources.

Sometimes for debugging you need to install contracts without running tests, in this case, you can do it by mvn clean install -DskipTests.

3.5. Different base classes for different test cases

Often we have test cases with a different initialization logic. For example, when you test a creating of something, you don’t need to prepare in the database the already created records. But if you test a reading of something from a database, then you need to be sure that read data sets exist in the database.

You can set a package with the base classes for tests generate, instead of setting a one base class for all tests.

<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>2.1.0.BUILD-SNAPSHOT</version>
    <extensions>true</extensions>
    <configuration>
        <packageWithBaseClasses>com.antkorwin.taskservice.api.contract</packageWithBaseClasses> (1)
        <testFramework>JUNIT5</testFramework>
    </configuration>
</plugin>
1 package with the base classes

And now, you need to keep your contracts directory in the next kind:

contracts directory

The names of your base classes depend on the structure of the contracts package, you need to reflect the names of directories in the name of the base class.

different base classes

And you will get the next generated tests structure:

generated test sources

Also when you write several contracts in one package, then you get a test class with multiple test cases. All of these items were named like packages and files in the resource directory:

So, we got a next tests after run generate-test-sources:

public class ReadTest extends TasksReadBase {  (1)

  @Test
  public void validate_task_list() throws Exception {
    ...
  }
}

public class WriteTest extends TasksWriteBase {  (2)

  @Test
  public void validate_create_task() throws Exception {
    ...
  }

  @Test
  public void validate_create_subtask() throws Exception {
    ...
  }
}
1 extends from the first base class (for tests of reading)
2 extends from the second base class (for tests of writing)

4. Consumer configuration

To be sure of the contract on the consumer side, we need to write tests on the communication layer between services (which used RestTemplate or Feign). In this tests, we will use a mock of the server which is started from the downloaded stub of contracts.

4.1. Dependency

The configuration of dependencies looks simpler on the consumer side than of the provider. All what we need is the stub runner dependency:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
</dependency>

4.2. Test with a stub of contracts

In order to use a stub in tests, we need to use the @AutoConfigureStubRunner annotation:

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.antkorwin:task-service:+:stubs:8585"},  (1)
                         stubsMode = StubRunnerProperties.StubsMode.LOCAL)   (2)
class TaskServiceFeignIT {

    @Autowired
    private TaskServiceFeign taskServiceFeign;

    @Test
    void getAllTasks() {
        List<Task> tasks = taskServiceFeign.getAllTask();
        assertThat(tasks).isNotNull()
                         .extracting(Task::getEstimate)
                         .contains(20, 5, 100);
    }
}
1 binary artifact with the stub of contracts
2 LOCAL/REMOTE mode

As you can see the ids: com.antkorwin:prediction-service:+:stubs:8585 is contain of :

stub ids

If you select a LOCAL mode, the stub runner will find binary artifact with contracts in your local maven repository (~/.m2/). Else if you select a REMOTE mode, you will need to set up the repositoryRoot where the stub runner will find a jar with contracts (we will consider the REMOTE configuration in more details a little later).

Let’s run this test with a local configuration:

consumer test result

5. How to use the Stub Runner (manual)

You can download stub runner server here: spring-cloud-contract-stub-runner-boot-2.0.1.RELEASE.jar. This is a simple Spring Boot application(made by Spring Cloud team) that starts a web server with API which described in the stub of contracts.

let’s run the next command to start the stub runner with contracts of the task-service on the 7000’s port:

$ java -jar ../spring-cloud-contract-stub-runner-boot-2.0.1.RELEASE.jar --stubrunner.stubs-mode="local" --stubrunner.ids=com.a ntkorwin:task-service:+:stubs:7000

then you can send a request to stub server:

$ curl -X POST '127.0.0.1:7000/tasks/create?title=Foo&estimate=123'

If you send a wrong request or make some typos in the URL or request parameters, the stub runner will find a more closest variant of API which existed in the loaded stub and will show it for you:

stub runner jar tool manual run

6. How to use Spring Cloud Contracts with a remote repository and configure CI system.

Now, we can make a contract and we can build and install it locally, the next step is a deploy this contract in the remote repository and sharing stubs between the different team of developers. We will use a JFrog Artifactory as a binary repository and TeamCity as a CI server.

teacity and artifactory in docker

6.1. Run TeamCity and Artifactory with docker-compose

All that we need is to run the next docker compose file:

version: '3'
services:
  teamcity-server-instance:
    image: jetbrains/teamcity-server
    ports:
     - "127.0.0.1:8111:8111"
    restart: always
    volumes:
      - ./test/teamcity/data:/data/teamcity_server/datadir
      - ./test/teamcity/logs:/opt/teamcity/logs
    networks:
      - teamcity-network

  teamcity-agent-instance:
    image: jetbrains/teamcity-agent
    restart: always
    volumes:
      - ./test/teamcity/agent-conf:/data/teamcity_agent/conf
    environment:
      - SERVER_URL=teamcity-server-instance:8111
    networks:
      - teamcity-network

  artifactory-instance:
    image: docker.bintray.io/jfrog/artifactory-oss:latest
    ports:
      - "127.0.0.1:8081:8081"
    restart: always
    networks:
      - teamcity-network

networks:
  teamcity-network:

Here we start a TeamCity Server and TeamCity Agent on the same network, after this, we will start Artifactory.

$ docker-compose up

Starting tc_artifactory-instance_1     ... done
Starting tc_teamcity-agent-instance_1  ... done
Starting tc_teamcity-server-instance_1 ... done

...

6.2. TeamCity & Artifactory configuration

When the all services successfully started, you need to configure TeamCity and create maven repository in the Artifactory.

As you can see on the next video, you need:

  • install artifactory teamcity plugin

  • setup connection from TeamCity to Artifactory

  • install Maven repository in the Artifactory

  • configure build step of the project in TeamCity, to deploy artifacts in the configured Artifactory instance

teamcity configuration with artifactory

6.3. Use the remote repository in the consumer tests

Now we need to set up a remote repository URL for stub runner:

@EnableIntegrationTests
@AutoConfigureStubRunner(ids = {"com.antkorwin:task-service:+:stubs"},
                         stubsMode = StubRunnerProperties.StubsMode.REMOTE,
                         snapshotCheckSkip = true,
                         repositoryRoot = "http://192.168.0.8:8081/artifactory/libs-snapshot-local") (1)
class TaskServiceFeignIT {

    @Autowired
    private TaskServiceFeign taskServiceFeign;

    @Test
    void getAllTasks() {
        List<Task> tasks = taskServiceFeign.getAllTask();
        assertThat(tasks).isNotNull()
                         .extracting(Task::getEstimate)
                         .contains(20, 17, 100);
    }
}
1 Use an address of artifactory instance

When you commit something in the git repository, TeamCity fetch it, build and deploy artifacts in the Artifactory, you can see all this sequence on the next demo:

deploy after commit on github

7. Source code of this project on the github