A sample project showing how to use Pact-JVM to implement consumer-driven tests and Consumer-Driven Contracts in Groovy.
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 beOK
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 acceptsGET
calls. When aGET /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.
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.
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
andcurrentDateTime
;
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"
}
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
}
}
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:
-
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;
-
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; -
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;
(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; andcurrentDateTime
, which is supposed to hold a string inyyyy-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.
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.
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.
We're going to use 2 plugins to help achieving our goal:
- the aforementioned Pact Gradle plugin, together with
- Gradle process plugin, which takes care of forking and keeping references to processes started within our build;
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:
pactPublish
: used to push all pact files in a directory to a pact broker (see https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-gradle#publishing-pact-files-to-a-pact-broker-version-227);pactVerify
: verifies all configured pacts against the producer;pactVerify_StatusEndpoint
: only verifiesStatusEndpoint
pact compliance;
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 :)
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.
These are most of the sources of information I've used to implement this example:
- https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-consumer-groovy
- http://www.chuanchuanlaw.com/pact-how-to-write-consumer-test/
- http://www.chuanchuanlaw.com/pact-how-to-write-provider-test/
- http://dius.com.au/2016/02/03/microservices-pact/
- https://github.com/mstine/microservices-pact
- https://github.com/DiUS/pact-workshop-jvm
- https://github.com/realestate-com-au/pact-jvm-provider-spring-mvc
- https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-provider-junit