Consumer-Driven Contracts with Pact-JS

Felipe Carvalho
techbeatscorner
Published in
5 min readJul 10, 2017
A kiss is also a kind of pact, no?

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…

  1. relies on a /status endpoint made available by the producer;
  2. 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 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.

--

--