Consumer-Driven Contracts with Pact-JS
On the first post of this series about Pact, we've implemented the concept of Consumer-Driven Contracts between a CLI in Groovy (representing the consumer of a service) and a HTTP API (its producer counterpart).
That first post was great to demo the concepts involved in this paradigm, and in this post we're going to see how that could work in a more recurring setting. We're going to build a very simple Web UI using React, and we're going to implement Consumer-Driven Contract using JavaScript.
code structure
As in the first post, the sources are available at https://github.com/felipecao/pact-sample. The new React project can be found under the /consumer-react
folder.
You’ll notice this project has just one React component: App.js
. You might also notice that, just like in the first post, this consumer relies on the existence of an 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"}
).
The App.js
component will issue a GET
request against that endpoint every second and will display the results on the screen. As usual, something very simple. The focus is on Pact-JS, not on React (I’m sorry if you read all the way here expecting to see some super fancy React code).
implementing the idea
step 0: creating the project
Facebook guys were kind enough to come up with a code generator that spits out a basic React app. Unsurprisingly, this generator is called create-react-app.
After installing it via npm
, just issue create-react-app consumer-react
on the command line. A skeleton project is created containing basically two files: App.js
and its unit test App.test.js
, both under /src
. Those are the files that matter the most for now.
step 0: laying the groundwork before creating the Pact
So, here’s the tricky part. By default, the project created by create-react-app
relies on Jest as test runner. And as soon as you bring Pact into the project and import it for the first time, strange things start to happen.
For instance, if you start following Pact-JS’ official documentation, add mocha into the project and try to run all tests without changing anything, you’re very likely running into this problem with dtrace-provider.
My intention here was not to come up with the most perfect solution for React to work with Pact-JS. Instead, I wanted to keep focus on Pact-JS and keep as close as possible to the official documentation.
Hence, instead of relying on the default test
script to run both the generated unit test and Pact tests, I decided to create a separate script on package.json
that would use mocha as test runner, avoiding Jest and the dread dtrace-provider
issue altogether. To keep things completely separated (and also easier to find within the project), I decided to have a separate folder exclusively for pacts, called… /pacts
! Tada! Who could’ve seen that one coming, huh?
So, this is how my scripts
section ended up looking like:
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js --env=jsdom",
"pacts": "BABEL_ENV=pacts ./node_modules/.bin/mocha --recursive --compilers js:babel-core/register \"pacts/**/*.pact.js\" --timeout 10000"
},
(You might notice that, by default, a project created with create-react-app
will result in a slightly different package.json
and some less folders, when compared to this project. That’s because I wanted to see what the react-scripts
dependency was hiding, and then decided to run npm run eject
, which caused all the structural differences.)
step 1: creating the Pact
Once all configuration is in place to allow us running Pact-JS, we’re good to start creating the new Pact.
As stated in the first post, the consumer project…
- 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 this consumer and the producer parties.
StatusEndpoint.pact.js
below depicts how such contract is proposed on behalf of our React consumer:
'use strict';let request = require('superagent');
let path = require('path');
let chai = require('chai');
let pact = require('pact');
let chaiAsPromised = require('chai-as-promised');
let expect = chai.expect;
let { term } = pact.Matchers;chai.use(chaiAsPromised);describe('StatusFrontEnd pact with StatusEndpoint', () => { const MOCK_PORT = Math.floor(Math.random() * 999) + 9000; const PROVIDER = pact({
consumer: 'StatusFrontEnd',
provider: 'StatusEndpoint',
port: MOCK_PORT,
log: path.resolve(process.cwd(), 'logs', 'pact.log'),
dir: path.resolve(process.cwd(), 'pacts'),
logLevel: 'INFO',
spec: 2
}); const EXPECTED_BODY = {
status: 'OK',
currentDateTime: '2017-07-04T17:02:53.582'
}; before((done) => {
PROVIDER.setup().then(() => {
PROVIDER.addInteraction({
given: 'status endpoint is up',
uponReceiving: 'a status enquiry',
withRequest: {
method: 'GET',
path: '/status',
headers: { 'Accept': 'application/json' }
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
status: 'OK',
currentDateTime: term({
generate: '2017-07-04T17:02:53.582',
matcher: '\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}'
})
}
}
})
})
.then(() => done());
}); after(() => {
PROVIDER.finalize()
}); it('returns the expected body', (done) => {
request
.get(`http://localhost:${MOCK_PORT}/status`)
.set({ 'Accept': 'application/json' })
.then((response) => {
expect(response.body.status).to.eql('OK');
expect(response.body.currentDateTime).to.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}/);
})
.then(done);
}); it('successfully verifies', () => PROVIDER.verify());});
Once you run npm run pacts
, a new file is created: /pacts/statusfrontend-statusendpoint.json
.
step 2: keeping the producer compliant with the new Pact
Just like in the previous post, let’s keep this simple for now. Let’s just copy the contract file created on the previous step and paste it on the producer side, under the /pacts
folder.
Once the new file is there, we just need to add a new entry to build.gradle
:
pact {
serviceProviders {
StatusEndpoint {
startProviderTask = 'startProducer'
terminateProviderTask = 'stopProducer' hasPactWith('StatusCLI') {
pactFile = file('pacts/StatusCLI-StatusEndpoint.json')
} // new entry here
hasPactWith('StatusFrontEnd') {
pactFile = file('pacts/statusfrontend-statusendpoint.json')
} }
}
}
And then, if we run ./gradlew pactVerify
, we should get a BUILD SUCCESSFUL
message.
Enough cut & paste
By now, you should probably be thinking all this cut & pasting of contracts among projects is not going to work in the long run, right?
Eventually, someone’s going to add a new clause to contract and will end up forgetting to put a copy of it on the producer project.
How can we improve this? By bringing a Pact broker in the picture. But that’s the theme of the next post.