/pact-sample

A sample project showing how to use Pact-JVM to implement consumer-driven tests in Groovy.

Primary LanguageJavaScriptApache License 2.0Apache-2.0

pact-sample

A sample project showing how to use Pact-JVM to implement consumer-driven tests and Consumer-Driven Contracts in Groovy.

the idea

With the increasing popularity of microservices, it can be quite hard to track the impact of a change done to a given service over its consumers. Consumers of this service can be other microservices or user interfaces.

A quite common setting is to have a RESTful Web API acting as a backend component to single-page app, which, in turn, acts as an UI to the end user. In such a setting, breaking changes can be introduced to the backend without the team responsible for the frontend being aware of it, causing disturbances to the experience provided to user.

One way of overcoming this issue is to use Consumer-Driven Contracts. Such technique proposes that the consumer of the information defines a contract between itself and the producer of the information, and both parties should conform to that contract at all times.

This project is to demonstrate in a very simple and concise manner how to implement Consumer-Driven Contracts between two parties.

You'll notice there are two sub-folders here:

  • consumer: a very simple command-line interface that accepts only one command: status. When invoked, this interface will connect to a backend component via HTTP, retrieve information about its availability and display it to the user. Two pieces of information should be displayed to the user: the backend status (it should be OK at all times, hopefully) and the date when that information was provided. Pretty basic, very very simple, our focus is not in showing off CLI skills, but rather to show how Consumer-Driven Contracts work on the client side.

  • producer: a very basic Spring-Boot service, with just one endpoint (/status) that accepts GET calls. When a GET /status request is issued against this service, a JSON response should be sent back (e.g.: {"status":"OK","currentDateTime":"2017-06-27T13:54:29.214"}). Again, very basic, very simple and minimalistic, the focus is not to come up with a super fancy service, but rather to demonstrate how a backend component can comply with a contract defined by its consumers.

implementing the idea

step 0: creating the project

Well, the technique is called "Consumer-Driven Contracts" for a reason. So I guess it makes sense to start by the consuming part of the project :)

So, to create the project, I just used Gradle to create a basic project: gradle init --type groovy-library. A small project containing a Library class, with its respective test was created.

After removing some of the code generated by Gradle and adding the dependencies to use Pact-JVM with Groovy, build.gradle looks like this:

apply plugin: 'groovy'

repositories {
    jcenter()
}

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.4.11'

    testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
    testCompile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7'
    testCompile 'au.com.dius:pact-jvm-consumer-groovy_2.11:3.5.0'
    testCompile 'au.com.dius:pact-jvm-consumer-junit_2.11:3.5.0'
}

In case you're asking what's http-builder is doing there, it's where Groovy's RESTClient sits, and this will be quite handy to implement the remote calls needed for the test.

step 1: creating the Pact

As previously stated, the consumer component...

  • relies on a /status endpoint made available by the producer;
  • expects such endpoint to accept GET calls and return a JSON object containing two attributes: status and currentDateTime;

Such expectations should then be clearly stated in the contract to be held between consuming and producing parties.

StatusEndpointPact.groovy below depicts how this contract is proposed by the consumer.

package pacts

import au.com.dius.pact.consumer.PactVerificationResult
import au.com.dius.pact.consumer.groovy.PactBuilder
import groovyx.net.http.RESTClient
import org.junit.Test

import java.time.format.DateTimeParseException

import static java.time.LocalDateTime.now
import static java.time.LocalDateTime.parse
import static java.time.format.DateTimeFormatter.ofPattern

class StatusEndpointPact {

    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"

    @Test
    void "pact for /status"() {
        def statusEndpointPact = new PactBuilder()

        statusEndpointPact {
            serviceConsumer "StatusCLI" 	        // Define the service consumer by name
            hasPactWith "StatusEndpoint"            // Define the service provider that the consumer has a pact with
            port 1234                               // The port number for the service. It is optional, leave it out to use a random one

            given('status endpoint is up')
            uponReceiving('a status enquiry')
            withAttributes(method: 'get', path: '/status')
            willRespondWith(status: 200, headers: ['Content-Type': 'application/json'])
            withBody {
                status "OK"
                currentDateTime timestamp(DATE_TIME_PATTERN, now().toString())
            }
        }

        // Execute the run method to have the mock server run.
        // It takes a closure to execute your requests and returns a PactVerificationResult.
        PactVerificationResult result = statusEndpointPact.runTest {
            def client = new RESTClient('http://localhost:1234/')
            def response = client.get(path: '/status')

            assert response.status == 200
            assert response.contentType == 'application/json'
            assert response.data.status == 'OK'
            assert dateTimeMatchesExpectedPattern(response.data.currentDateTime)
        }

        assert result == PactVerificationResult.Ok.INSTANCE  // This means it is all good
    }

    private boolean dateTimeMatchesExpectedPattern(String currentDateTime) {
        try {
            parse(currentDateTime, ofPattern(DATE_TIME_PATTERN))
        } catch (DateTimeParseException e) {
            return false
        }

        return true
    }
}

(You'll notice my example has a lot in common with the example proposed on https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-consumer-groovy :) )

From this point on, you have your first consumer-driven test. To run it, simply right-click the class on your favourite IDE and run it. No need to rely on special fancy Gradle commands, any regular test running mechanism will do, such ./gradlew test.

As soon as you run this test, a new file will be created: target/StatusCLI-StatusEndpoint.json. This file describes the contract both parties should comply with, along with Pact-specific metadata.

Having something generated under /target on a Gradle project sounds rather funky. You can customize this via system properties. Suppose you want generated pacts to be placed under Gradle's regular /build folder, or under /build/pacts. Just invoke your test using -Dpact.rootDir="build/pacts". Or if you rather have this configured at build.gradle, add the following block:

test {
    systemProperties['pact.rootDir'] = "$buildDir/pacts"
}

step 1.1: making some sense out of consumer needs

It's a bit weird to just define a contract without an use case to back it up, right?

As previously stated, the consumer is supposed to be a very simple command-line interface that accepts only one command: status. So, just to add more context and improve understanding of the consumer needs, the classes below depict what the consumer offers to the end user. (Please notice this requires you to go back to build.gradle and change http-builder to be a compile dependency, instead of testCompile).

These are the classes on the consumer project:

Main.groovy

package com.github.felipecao.pactsample

import com.github.felipecao.pactsample.cli.CommandLineInterface
import com.github.felipecao.pactsample.provider.StatusClient

class Main {
    static void main(String[] args) {
        StatusClient statusClient = new StatusClient()
        InputStream inputStream = System.in
        CommandLineInterface cli = new CommandLineInterface(statusClient, inputStream)

        cli.run()
    }
}

CommandLineInterface.groovy

package com.github.felipecao.pactsample.cli

import com.github.felipecao.pactsample.provider.StatusClient

class CommandLineInterface {

    private static final STATUS_COMMAND = "status"

    private static final QUIT_COMMAND = "quit"

    private StatusClient statusClient

    private InputStream inputStream

    CommandLineInterface(StatusClient statusClient, InputStream inputStream = null) {
        this.statusClient = statusClient
        this.inputStream = inputStream ?: System.in
    }

    void run() {
        inputStream.withReader {
            while (true) {
                String userCommand = readUserInput(it)

                if (userCommand.equalsIgnoreCase(QUIT_COMMAND)) {
                    System.exit(0)
                }

                if (!userCommand.equalsIgnoreCase(STATUS_COMMAND)) {
                    println("Command '${userCommand}' is not supported. Try '${STATUS_COMMAND}' or '${QUIT_COMMAND}' instead.")
                    continue
                }

                println(statusClient.retrieveProviderStatus())
            }
        }
    }

    private String readUserInput(Reader reader) {
        print "Enter command: "
        return reader.readLine().trim()
    }
}

StatusClient.groovy

package com.github.felipecao.pactsample.provider

import groovyx.net.http.RESTClient

class StatusClient {
    private static final String BASE_URL = "http://localhost:8080"
    private RESTClient restClient

    StatusClient() {
        this.restClient = new RESTClient(BASE_URL)
    }

    def retrieveProviderStatus() {
        restClient.get([path: '/status']).data
    }
}

step 2: making the Pact available to the producer

After having the Pact contract defined by the consumer, it makes sense to have the producer comply to it, no? So, the next step is having both the consumer and the producer look at the same contract.

If you look at https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-consumer-groovy and https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-junit, there a few ways to implement this:

  1. The best approach is to have your contracts available at some kind of broker. https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-consumer-groovy#publishing-your-pact-files-to-a-pact-broker talks a little bit about it. In this setting, as soon as a pact is generated, it's uploaded to Pact broker, from which the producer can afterwards download the same pact and make sure the contract is being complied with. https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-junit#download-pacts-from-a-pact-broker shows how to download a Pact file from a broker;

  2. Publish the Pact file somewhere in your network and make it available to both producer and consumer. In this case, you can use either @PactUrl or @PactFolder annotations to link your producer tests to the contracts;

  3. The most basic one is obviously cutting the pact generated by the consumer and pasting it at a location visible to the producer. It's definitely not ideal and can cause many synchronisation problems, but it's definitely the simplest one;

step 3: implementing Pact compliance on the producer

(Even though it can't be considered a good practice, just for the sake of simplicity, we'll copy the Pact file generated on the consumer project and paste it on the producer project. Please don't tell anyone I've given such a dread example :) )

Ok, so now we already know what our producer is supposed to do. Its consumers have said they want a service that responds to GET requests at /status endpoint. They have also stated they expect the response body to contain two attributes:

  • status, which, by the way, is supposed to be a string; and
  • currentDateTime, which is supposed to hold a string in yyyy-MM-dd'T'HH:mm:ss.SSS format.

You could take two approaches from here: start with the usual TDD cycle (create a test, have it fail, make it work), which would be awesome; or write implementation first. As I'm doing this for the first time, for the sake of simplicity, I'll go with the implementation first.

step 3.1: writing the production code on the consumer side

To support such needs from consumers, I decided to go with a very simple Spring-Boot app written in Groovy.

This is how build.gradle looks like:

buildscript {
	ext {
		springBootVersion = '1.5.4.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'groovy'
apply plugin: 'org.springframework.boot'

version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
	mavenCentral()
}

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

And this how the controller looks like:

package com.github.felipecao.pactsample.producer

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController

import java.time.LocalDateTime

@RestController
@CrossOrigin
class StatusController {

    @RequestMapping(value = "/status", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    Map currentStatus() {
        [status: "OK", currentDateTime: LocalDateTime.now().toString()]
    }
}

Very basic stuff, nothing too fancy, the focus here is not on backend code, but rather on complying with the Pact.

step 4: complying with the Pact

There are many possible approaches to this task. https://github.com/DiUS/pact-jvm#i-am-writing-a-provider-and-want-to- lists a lot of them.

Given the producer service is a Spring-Boot app (which uses Spring MVC under the hood to support HTTP calls), one could say it makes sense to go with Pact Spring MVC Runner for implementing the pact-compliance tests.

What I personally don't like about this approach is the fact that you end up with a unit tests, having controller dependencies mocked, etc. In a real world scenario, where many other components would be acting as consumers to my producer, I'd personally feel more comfortable with having a broader scoped test guaranteeing that everything is fine on my service, so I'll skip Pact Spring MVC Runner for now. (Please notice this is just a personal preference, you should pick whatever makes more sense to your project).

Instead, I thought it'd be interesting to go with Pact Gradle plugin. In such approach, you'd start your producer, have the pact verification take place and afterwards kill your producer. You'll go through all layers, from controller all the way to the DB (if you have it) and back. I feel more comfortable with this approach for contract validation.

step 4.1: using Pact Gradle plugin to comply with the Pact

We're going to use 2 plugins to help achieving our goal:

This is how the top part of build.gradle looks like after adding the plugins:

buildscript {
	ext {
		springBootVersion = '1.5.4.RELEASE'
	}
	repositories {
        jcenter()
        mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
		classpath("au.com.dius:pact-jvm-provider-gradle_2.11:3.5.0")
		classpath("com.github.jengelman.gradle.plugins:gradle-processes:0.3.0")
	}
}

apply plugin: 'groovy'
apply plugin: 'org.springframework.boot'
apply plugin: 'au.com.dius.pact'
apply plugin: 'com.github.johnrengelman.processes'

Pact plugin configuration is pretty straightforward, the most basic configuration you'd need is:

pact {
    serviceProviders {
        StatusEndpoint {
            hasPactWith('StatusCLI') {
                pactFile = file('pacts/StatusCLI-StatusEndpoint.json')
            }
        }
    }
}

In our case, we want to start our producer Spring-Boot app and have the Pact being checked against it, and that's where gradle-processes comes in handy. We're going to use it to start and stop the service:

task startProducer(type: JavaFork) {
    classpath = sourceSets.main.runtimeClasspath
    main = 'com.github.felipecao.pactsample.producer.Application'

    doLast {
        Thread.sleep(15000) // time Spring Boot takes to start -- you'll end up with an error saying 'Connection refused' if you don't have this...
    }
}

task stopProducer << {
    startProducer.processHandle.abort()
}

pact {
    serviceProviders {
        StatusEndpoint {
            startProviderTask = 'startProducer'
            terminateProviderTask = 'stopProducer'
            hasPactWith('StatusCLI') {
                pactFile = file('pacts/StatusCLI-StatusEndpoint.json')
                // notice the dreadful copy & paste of the Pact file from the consumer project into the producer project.
            }
        }
    }
}

And that's it, we already have everything in place to check if our producer matches our consumer expectations.

Notice Pact Gradle plugin introduces the a few tasks into Gradle lifecyle:

To make it simple, we'll just run ./gradlew pactVerify on the producer project, and there you go, you have producer and consumer signing a pact :)

considerations about continuous integration

The nice thing about using Pact Gradle plugin is that you can quite easily run your Pact verifications on a completely separate CI cycle from your regular tests. You can have your regular unit tests providing fast feedback to the team, and have your pacts automatically checked on your favourite CI tool every once in a while.

This is especially important considering that:

  • the presented Gradle setup waits 15 seconds before starting the actual pact tests; if those tests were run on the regular CI build, it would really slow down the feedback cycle to everyone on the team;
  • as the application grows and starts taking longer to start, this wait time will need to be adjusted, making the feedback cycle even longer.

On the other hand, it might not always be desirable to have such separation in place. Depending on the situation at hand, your team might prefer having all tests running all together. But you definitely don't want to wait 15 more seconds to obtain feedback from your CI cycle. You already have integration tests running in your test suite, and they take long enough. Waiting longer for feedback shouldn't be an option.

What you can do to have the best of both worlds is use the power of dynamic languages (like Groovy) to read the Pact JSON file and combine its contents with SpringMVC mock facilities. Think about it: your integration tests already start the container anyway, why restart the container and wait another 15+ seconds to perform Pact validations? Why not just build on top of your existing controller integration tests?

With that in mind, there's a VERY VERY SIMPLE AND PROTOTYPICAL (it's important to highlight this point before telling me the code sucks and is full of flaws. It is supposed to be full of flaws :P) proposal in this project to tackle that problem.

Have a look at StatusControllerIntegrationTest and the other classes below:

StatusControllerIntegrationTest.groovy

package com.github.felipecao.pactsample.producer

import com.github.felipecao.pact.Interactions
import com.github.felipecao.pact.Pact
import com.github.felipecao.pact.PactExecutor
import org.junit.Before
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.test.context.junit4.SpringRunner
import org.springframework.test.context.web.WebAppConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.web.context.WebApplicationContext

import java.nio.file.Path
import java.nio.file.Paths

import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup

@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
class StatusControllerIntegrationTest {

    private static final Path PACT_FILE = Paths.get("pacts", "StatusCLI-StatusEndpoint.json")

    private MockMvc mockMvc

    private PactExecutor pactExecutor

    private Interactions interactions

    @Autowired
    private WebApplicationContext webApplicationContext

    @Before
    void setup() throws Exception {
        this.mockMvc = webAppContextSetup(webApplicationContext).build()
        this.pactExecutor = new PactExecutor(this.mockMvc)
        this.interactions = new Interactions(new Pact(PACT_FILE))
    }

    @Test
    void "verify status pact"() throws Exception {
        pactExecutor.verify(interactions.withDescription("a status enquiry"))
    }
}

PactExecutor.groovy

package com.github.felipecao.pact

import org.springframework.test.web.servlet.MockMvc

import java.nio.file.Path

import static com.github.felipecao.pact.matcher.TimestampMatcher.matchesPattern
import static org.hamcrest.Matchers.is
import static org.hamcrest.core.StringStartsWith.startsWith
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*

class PactExecutor {
    private MockMvc mockMvc

    PactExecutor(MockMvc mockMvc) {
        this.mockMvc = mockMvc
    }

    void verify(def interaction) {
        def timestampPattern = interaction.response.matchingRules.body.'$.currentDateTime'.matchers[0].timestamp

        mockMvc.perform(get(interaction.request.path))
                .andExpect(status().is(interaction.response.status))
                .andExpect(status().is(interaction.response.status))
                .andExpect(header().string("Content-Type", startsWith(interaction.response.headers."Content-Type")))
                .andExpect(jsonPath('$.status').value(is(interaction.response.body.status)))
                .andExpect(jsonPath('$.currentDateTime').value(matchesPattern(timestampPattern)))
    }

}

Pact.groovy

package com.github.felipecao.pact

import groovy.json.JsonSlurper

import java.nio.file.Path

class Pact {
    private def json

    Pact(Path pactFile) {
        def jsonSlurper = new JsonSlurper()
        json = jsonSlurper.parse(pactFile.toFile())
    }

    def findInteraction(String description) {
        json.interactions.find {it.description == description}
    }
}

Interactions.groovy

package com.github.felipecao.pact

class Interactions {
    private Pact pact

    Interactions(Pact pact) {
        this.pact = pact
    }

    def withDescription(String description) {
        pact.findInteraction(description)
    }
}

These classes build on top on Groovy's dynamism to provide an easy-to-read way to parse the Pact file. Combined with SpringMVC's MockMvc interface, this is a lightweight approach that reuses Spring-Boot integration tests to also check pacts.

If you like this approach and would consider using it in your team, I'd advise you to be VERY CAREFUL, as it usually doesn't pay off to maintain an in-house JSON parsing framework that relies on a 3rd party syntax (in this case, Pact syntax). Parsing Pact's matchingRules can be a particularly great PITA.

references

These are most of the sources of information I've used to implement this example: