5 October 2018 - on 

In this article we are going to see how to use a consumer contract generated with Pact to drive the development of a Spring Boot Microservice. This is the last of a series of articles in which I examined how to apply BDD and Consumer Driven Contracts in a Microservices environment. If you have not already read the previous articles, maybe is a good moment to do it first.

BDD in a Microservices Environment using Consumer Driven Contract Testing (1)  

Generating a Consumer Contract from an Angular application using Pact-JS

Rest API Gateway using Netflix Zuul and Pact Consumer Driven Contracts

You can find the code of the sample application in https://github.com/jagilpe/bdd-pact-microservices.

The backend microservices

At the end of the previous article we generated the contracts that will have to fulfill the backend microservices in which we divided the API. Now we are going to see how to we can drive the development of the backend services based on this contracts.

Pact broker after API Gateway implementation

 

We decided to divide our API in two services, each of them responsible for one part of the requests.

  • products-catalogue-service, responsible for the categorization of the products and to return their details.
  • offers-service, responsible for returning the offers corresponding to a determined product.

You can find the code of each service in the offers-service and product-catalogue directories of the example repository.

Now we are going to see in more detail how to implement the product-catalogue-service from its contract with the api-gateway using Spring Boot. 

Create the Spring Boot Application (1)

Create a directory product-catalogue that will be the root of the project, and create inside of it a file called build.gradle with the following content.

// product-catalogue/build.gradle
buildscript {
    ext {
        springBootVersion = '2.0.5.RELEASE'
        jarName = "product-catalogue-${version}.jar"
    }
    repositories {
        jcenter()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

version = '0.0.1-SNAPSHOT'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    jcenter()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

Now we have to create the directory src/main/java/productcatalogue and create the main SpringBoot application class inside of it:

// product-catalogue/src/main/java/productcatalogue/ProductCatalogueApplication.java
package productcatalogue;

 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;

 @SpringBootApplication
 public class ProductCatalogueApplication {
     public static void main(String[] args) {
         SpringApplication.run(ProductCatalogueApplication.class, args);
     }
 }

Setup the Consumer Driven Contract framework

To be able to use the pact contract generated in the end of the previous article for the implementation of the product-catalogue-service we are going to use the Pact JVM Provider for Spring library. We simply have to add the testing dependency in our gradle project:

// product-catalogue/build.gradle

//...

plugins {
    // ...
    id "au.com.dius.pact" version "3.2.13"
}

//...
dependencies {
    //...
    testCompile ('au.com.dius:pact-jvm-provider-spring_2.12:3.5.23')
}

Interactions test class

To run the tests that will check the interactions defined in the pact contract we have to create a test class and run it using the SpringRestPactRunner provided by the Pact JVM Provider library. Create a directory src/test/java/productcatalogue and create a file ProductCatalogueAppIT.java in it with the content:

// product-catalogue/src/test/java/productcatalogue/ProductCatalogueAppIT.java
package productcatalogue;

import au.com.dius.pact.provider.junit.Provider;
import au.com.dius.pact.provider.junit.State;
import au.com.dius.pact.provider.junit.loader.PactBroker;
import au.com.dius.pact.provider.junit.target.HttpTarget;
import au.com.dius.pact.provider.junit.target.Target;
import au.com.dius.pact.provider.junit.target.TestTarget;
import au.com.dius.pact.provider.spring.SpringRestPactRunner;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;

@RunWith(SpringRestPactRunner.class)
@Provider("products-catalogue-service")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = ProductCatalogueApplication.class )
@PactBroker(protocol = "http", host = "127.0.0.1", port = "1080")
public class ProductCatalogueAppIT {

    @TestTarget
    public Target target = new HttpTarget(8080);

}

Some notes about the basic configuration of Pact for checking a Spring Boot provider contract:

  • The test class has to be run using the SpringRestPactRunner
  • You have to give the name of the provider using the @Provider annotation. This has to match the name of the provider in the pact published in the Pact Broker
  • We use the @PactBroker annotation to configure the data regarding the Pact Broker server we use to share the pacts between the different consumers and providers (2)
  • We have to provide a test target annotated with the @TestTarget annotation, and configured with the port in which our Spring Boot application is going to run.

If you now run the tests (3) you will see that there are three failing tests (as expected because we haven't yet implemented anything of our application).

gradle test

> Task :test

productcatalogue.ProductCatalogueAppIT > api-gateway - A request for products in the category 1 FAILED
    au.com.dius.pact.provider.junit.MissingStateChangeMethod

productcatalogue.ProductCatalogueAppIT > api-gateway - A request for a list of categories FAILED
    au.com.dius.pact.provider.junit.MissingStateChangeMethod

productcatalogue.ProductCatalogueAppIT > api-gateway - a request for the details of a product FAILED
    au.com.dius.pact.provider.junit.MissingStateChangeMethod
2018-10-15 09:53:02.226  INFO 5289 --- [       Thread-6] ConfigServletWebServerApplicationContext : Closing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@7f5ca629: startup date [Mon Oct 15 09:52:58 CEST 2018]; root of context hierarchy

3 tests completed, 3 failed
...

If we look at the messages you can see that it's telling us that it's missing a state change method for each of the interactions our provider has to fulfill. If you go to the details of the pact in the pact broker you can see that for each interaction, there is an array providerStates that contains a descriptions of the initial states of the provider in order to validate the given interaction.

// http://localhost:1080/pacts/provider/products-catalogue-service/consumer/api-gateway/latest.json
{
  "provider": {
    "name": "products-catalogue-service"
  },
  "consumer": {
    "name": "api-gateway"
  },
  "interactions": [
    {
      "description": "A request for products in the category 1",
      // ...
      "providerStates": [
        {
          "name": "there are 5 products in the category 1"
        }
      ]
    },
    {
      "description": "A request for a list of categories",
      // ...
      "providerStates": [
        {
          "name": "there are 6 categories"
        }
      ]
    },
    {
      "description": "a request for the details of a product",
      // ...
      "providerStates": [
        {
          "name": "there is a product iPhone 8"
        }
      ]
    }
  ]
  // ...
}

We have to write a method annotated with @State for each of the providerStates that appear in the pact. In this method we have to run the code required to bring the provider to the state defined in it. Before it checks an interaction, the SpringRestPactRunner will run the methods corresponding to the states required by the interaction to test.

// product-catalogue/src/test/java/productcatalogue/ProductCatalogueAppIT.java
package productcatalogue;

import au.com.dius.pact.provider.junit.Provider;
import au.com.dius.pact.provider.junit.State;
import au.com.dius.pact.provider.junit.loader.PactBroker;
import au.com.dius.pact.provider.junit.target.HttpTarget;
import au.com.dius.pact.provider.junit.target.Target;
import au.com.dius.pact.provider.junit.target.TestTarget;
import au.com.dius.pact.provider.spring.SpringRestPactRunner;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;

@RunWith(SpringRestPactRunner.class)
@Provider("products-catalogue-service")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = ProductCatalogueApplication.class)
@PactBroker(protocol = "http", host = "127.0.0.1", port = "1080")
public class ProductCatalogueAppIT {

    @TestTarget
    public Target target = new HttpTarget(8080);

    @State("there are 5 products in the category 1")
    public void setUpStateThereAreFiveProductsInCategory1() throws Exception {
    }

    @State("there is a product iPhone 8")
    public void setUpStateThereIsAProductIPhone8() throws Exception {
    }

    @State("there are 6 categories")
    public void setUpStateThereAre6Categories() throws Exception {
    }
}

If you run again the test after creating the state change method, you'll find that now the error messages have changed.

gradle test

> Task :test

productcatalogue.ProductCatalogueAppIT > api-gateway - A request for products in the category 1 FAILED
    java.lang.AssertionError

productcatalogue.ProductCatalogueAppIT > api-gateway - A request for a list of categories FAILED
    java.lang.AssertionError

productcatalogue.ProductCatalogueAppIT > api-gateway - a request for the details of a product FAILED
    java.lang.AssertionError
2018-10-15 16:20:01.475  INFO 10866 --- [       Thread-6] ConfigServletWebServerApplicationContext : Closing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@1cc0fa67: startup date [Mon Oct 15 16:19:56 CEST 2018]; root of context hierarchy

3 tests completed, 3 failed

> Task :test FAILED
...

In this point we can start to implement each of the scenarios defined in the interactions of the provider contract using the methodology we want.

Implementation of the Product Catalogue Service

From this point, the implementation of the Product Catalogue Service is pretty standard and there are a lot of possible solutions and methodologies that can be used. I'll avoid going into too much detail in this because it's beyond the scope of the article, and I'll simply comment the most relevant points of the sample application for the second scenario "A request for a list of categories". For more details you can see the whole code in the example repository.

The sample application uses the RestControllers of Spring MVC for creating the Rest Web Service, and Spring Data JPA for the persistence. In order to initialize the database to the right state for the tests in the state change methods of the interaction classes it uses DBUnit.

First we have to add the required dependencies to the gradle project:

// product-catalogue/build.gradle

// ...
dependencies {
    // ...
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("com.h2database:h2")
    compileOnly('org.projectlombok:lombok')
    testCompile("org.dbunit:dbunit:2.5.4")
}

The CategoryController simply gets the list of categories from the CategoryService and returns it.

// product-catalogue/src/main/java/productcatalogue/controller/CategoryController.java
package productcatalogue.controller;

import productcatalogue.model.Category;
import productcatalogue.service.CategoryService;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping(path = "/categories")
@RequiredArgsConstructor
public class CategoryController {

    private final CategoryService categoryService;

    @RequestMapping(method = RequestMethod.GET)
    public Iterable<Category> getCategories() {
        return categoryService.findAll();
    }
}

The CategoryServices gets the categories from a Spring Data Repository.

// product-catalogue/src/main/java/productcatalogue/service/CategoryService.java
package productcatalogue.service;

import productcatalogue.model.Category;
import productcatalogue.persistence.CategoryRepository;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class CategoryService {

    private final CategoryRepository categoryRepository;

    public Iterable<Category> findAll() {
        return categoryRepository.findAll();
    }
}

The CategoryRepository is a simple Crud Spring Data Repository.

// product-catalogue/src/main/java/productcatalogue/persistence/CategoryRepository.java
package productcatalogue.persistence;

import productcatalogue.model.Category;
import org.springframework.data.repository.CrudRepository;

public interface CategoryRepository extends CrudRepository<Category, Long> {
}

Finally we use a data fixture loader to set up the database in the right state before each of the test cases using DBUnit. After each test the database is cleaned up calling reset method of the data fixture loader.

// product-catalogue/src/test/java/productcatalogue/ProductCatalogueAppIT.java
package productcatalogue;

import au.com.dius.pact.provider.junit.Provider;
import au.com.dius.pact.provider.junit.State;
import au.com.dius.pact.provider.junit.loader.PactBroker;
import au.com.dius.pact.provider.junit.target.HttpTarget;
import au.com.dius.pact.provider.junit.target.Target;
import au.com.dius.pact.provider.junit.target.TestTarget;
import au.com.dius.pact.provider.spring.SpringRestPactRunner;
import productcatalogue.util.DataFixtureLoader;
import productcatalogue.util.TestCase;
import org.junit.After;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@RunWith(SpringRestPactRunner.class)
@Provider("products-catalogue-service")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = ProductCatalogueApplication.class)
@PactBroker(protocol = "http", host = "127.0.0.1", port = "1080")
public class ProductCatalogueAppIT {

    @TestTarget
    public Target target = new HttpTarget(8080);
    
    @Autowired
    private DataFixtureLoader dataFixtureLoader;

    @After
    public void clearDatabase() throws Exception {
        dataFixtureLoader.reset();
    }
    
    @State("there are 5 products in the category 1")
    public void setUpStateThereAreFiveProductsInCategory1() throws Exception {
        dataFixtureLoader.setUpFixture(TestCase.FIVE_PRODUCTS_IN_CATEGORY_1);
    }

    @State("there is a product iPhone 8")
    public void setUpStateThereIsAProductIPhone8() throws Exception {
        dataFixtureLoader.setUpFixture(TestCase.ONE_PRODUCT);
    }

    @State("there are 6 categories")
    public void setUpStateThereAre6Categories() throws Exception {
        dataFixtureLoader.setUpFixture(TestCase.SIX_CATEGORIES);
    }
}

If we run the tests once again after we have finished the implementation of the three interactions of the contract, will see that now the tests are passing.

gradle clean test

> Task :test

productcatalogue.ProductCatalogueAppIT > api-gateway - A request for products in the category 1 PASSED

productcatalogue.ProductCatalogueAppIT > api-gateway - A request for a list of categories PASSED

productcatalogue.ProductCatalogueAppIT > api-gateway - a request for the details of a product PASSED
2018-10-16 11:12:41.169  INFO 7649 --- [       Thread-6] ConfigServletWebServerApplicationContext : Closing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@f75a335: startup date [Tue Oct 16 11:12:34 CEST 2018]; root of context hierarchy
2018-10-16 11:12:41.171  INFO 7649 --- [       Thread-6] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2018-10-16 11:12:41.172  INFO 7649 --- [       Thread-6] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed drop of schema as part of SessionFactory shut-down'
2018-10-16 11:12:41.176  INFO 7649 --- [       Thread-6] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2018-10-16 11:12:41.178  INFO 7649 --- [       Thread-6] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

BUILD SUCCESSFUL in 9s
4 actionable tasks: 4 executed

 

Foot notes

(1) There are different options to create the Spring Boot Application: using gradle, maven, Spring Initializr, eclipe, Intellij idea... In this case I'm using gradle directly, but the steps are easily transferable to the other options.

(2) If we would like to use a pact files instead of a broker we should point the location of the pacts using the @PactFolder annotation. You can find more information about this in the Pact JVM Spring Provider documentation

(3) In order to run the tests you must have a pact broker running on the port 1080, and it should already have the pacts generated by the API Gateway application.