At RisingStack we love working with MicroservicesMicroservices are not a tool, rather a way of thinking when building software applications. Let's begin the explanation with the opposite: if you develop a single, self-contained application and keep improving it as a whole, it's usually called a monolith. Over time, it's more and more difficult to maintain and update it without breaking anything, so the development cycle may..., as this kind of architecture gives us flexibility and speed. In this article I’ll walk you through how we perform consumer driven contract testing in our Node.jsNode.js is an asynchronous event-driven JavaScript runtime and is the most effective when building scalable network applications. Node.js is free of locks, so there's no chance to dead-lock any process. microservices architecture with the Pact framework.
The example repo can be found at https://github.com/RisingStack/pact-example.
What is consumer driven contract testing?
To summarize, contract testing means that we verify our API against a set of expectations (contracts). This means that we want to check if upon receiving a specific call, our API provider server will return the data we specified in the documentation. We often lack precise information on the needs our API consumers. To overcome this problem consumers can define their expectations as mocks that they use in unit tests, creating contracts that they expect us to fulfill. We can gather these mocks and verify that our provider returns the same or similar data when called the same way as the mock is set up, essentially testing the service boundary. This approach is called consumer driven contract testing.
What is Pact?
The Pact family of frameworks provide support for Consumer Driven Contracts testing. Source: https://docs.pact.io/
Why should we do contract testing?
Usually we want to move quickly with fast iterations, which means that we can try out ideas quickly and dispose of the ones that don’t work – so we don’t get stuck with bad decisions when it turns out there’s a better one.
However, as architectures grow, it can be difficult to figure out what broke what – especially when our service has multiple consumers. We can write integration tests to make sure that the service boundaries are safe, but those tend to be difficult and slow.
Another way is to write contract tests, which help us to make sure that we fulfill the contract that we provide to our consumers.
But what if a change has to be rolled out quickly, and we forget about contract testing?
We have a lot of responsibilities when we introduce changes: we have to make sure the new version does not introduce a breaking change, or if it does, we have to create a new version of the endpoint, document the updated API, write unit tests, write integration tests, and so on..
If we do not control all the consumers of our APIs, the exact needs of our consumers can get lost in translation. Even if our integration tests catch the problem, we might not know whether we caught a bug in the consumer, or we did not fulfill our contracts properly.
The good news is that our consumers surely have unit tests in place. These tests should run in isolation so all dependencies are ought to be mocked, including our API provider. These mocks essentially specify a contract they expect us to fulfill. Couldn’t we use those to make sure our API serves the data they need?
Yes we definitely can! That is called consumer driven contract testing.
When it comes to contract testing, Pact is the go-to tool these days. We can use it for mocking on the client side, and for sharing these mocks with the API providers. This way, the API providers can check if the changes they introduce would break anything downstream.
Let’s take a look at implementing such a solution!
Example app – client side
Let’s assume that we have a service that stores our available products, and it provides an API for querying them. Besides that, we also have a service that requests the list of available products and logs them to stdout
.
// client/client.js
const request = require('request-promise-native')
const _ = require('lodash')
const PRODUCTS_SERVICE_URL = process.env.PRODUCTS_SERVICE_URL || 'http://localhost:1234'
async function getAllProducts () {
const products = await request(`${PRODUCTS_SERVICE_URL}/products`)
.then(JSON.parse)
const productsString = _.reduce(products, (logString, product) => `${logString} ${product.name}`, 'CLIENT: Current products are:')
console.log(productsString)
}
module.exports = {
getAllProducts
}
Let’s test it!
Step 1.: Create a mock service with Pact
First, we need to create a mock service using the pact library from npmnpm is a software registry that serves over 1.3 million packages. npm is used by open source developers from all around the world to share and borrow code, as well as many businesses. There are three components to npm: the website the Command Line Interface (CLI) the registry Use the website to discover and download packages, create user profiles, and.... The mock server will take the role of the provider and respond to our requests in the way we define it. It will also record all our mocks and save them to a pact file, so we can share the created contract.
// client/mockServer/provider.js
const path = require('path')
const pact = require('pact')
const interactions = require('./interactions')
const provider = pact({
consumer: 'client', // current service at hand, it makes it easier to know who would be broken by the change in the provider when we test the contract.
provider: 'ProductService', // required, so we know who will need to verify the pact
port: 1234, // where the mock service should be listening
log: path.resolve(__dirname, '../../logs', 'mockserver-integration.log'), // path to the file where logs should be stored
logLevel: 'ERROR', // one of 'TRACE', 'DEBUG', 'INFO', 'ERROR', 'FATAL' OR 'WARN'
dir: path.resolve(__dirname, '../../pacts'), // path to the files where the pact should be saved
spec: 2 // the pact specification we are using
module.exports = provider
}
The interactions are defined in a separate mock file because we want to reuse them as much as possible.
// client/mockServer/interactions.js
const ONE_PRODUCT_BODY = [{ name: 'Foo' }]
module.exports = {
getProductList: {
state: 'it has one product',
uponReceiving: 'a request to retrieve product list',
withRequest: {
method: 'GET',
path: '/products'
},
willRespondWith: {
status: 200,
body: ONE_PRODUCT_BODY
}
}
}
It looks just like any http mock we would write with nock or any other library. Note, however, that all reusable parts such as the body
should be defined in constants.
Imagine, we forget later on that we expect the returned object to have a name
field and we mistakenly mock it as title
instead. Maybe our app is capable of handling both, and we would force the provider to duplicate the data under different field names for no reason whatsoever.
Step 2: Writing our tests with our usual tools: sinon
& chai
Now we have a proper mock service, let’s write our tests!
// client/client.spec.js
const chai = require('chai')
const sinon = require('sinon')
const sinonChai = require('sinon-chai')
const provider = require('./mockServer/provider')
const client = require('./client')
const expect = chai.expect
chai.use(sinonChai)
describe(‘product handling, () => {
const sandbox = sinon.createSandbox()
before(async function () {
this.timeout(10000) // it takes time to start the mock server
await provider.setup()
})
afterEach(() => {
sandbox.restore()
})
after(async function () {
this.timeout(10000) // it takes time to stop the mock server and gather the contracts
await provider.finalize()
})
describe('#getAllProducts', () => {
it('should get product list from server', async function () {
await provider.addInteraction(interactions.getProductList)
const consoleSpy = sandbox.spy(console, 'log')
await client.getAllProducts()
expect(consoleSpy).to.have.been.calledWith('CLIENT: Current products are: Foo')
await provider.verify()
})
})
})
As you can see, our test mostly looks the same as it would otherwise. The only trace of pact is in the before and after hooks, in the provider.addInteraction(interactions.getProductList)
line in the beginning, and the provider.verify()
line at the end of the test case.
Of course, we need to add the interactions we want to mock first, and then we need to verify that they were actually called during the course of the test.
Before running any of the test cases, we need to set up the mock service (make sure to raise the timeout here, as it might take a couple of seconds) and in the end, we need to gather the mocks into pacts and save them to a file.
If we run the test, we will see some output from pact while it sets up the server, but afterwards we see the output from mocha that we already got used to. We have two folders created: pacts
and log
. In pacts we can see the pact created that we can use to test our provider.
Step 3.: Using Pact Broker to share our pacts
So far so good. But we need a way to share our pacts with the provider as well. To do so, you can use pact-broker.
For the purposes of this post, we will use an image with sqlite3 but if you plan to use it in your workflow, make sure to have a proper postgres db ready that pact broker can use.
$ docker run -d -p 8080:80 risingstack/pact_broker_example
# or
$ npm run pact-broker # in the example repo
Now the broker is available at http://localhost:8080
. It already has an example pact, but we don’t need it, so let’s get rid of it
$ curl -X DELETE http://localhost:8080/pacticipants/Zoo%20App
# or
$ npm run delete-example-pact # in the example repo
If you don’t want to use curl, you can use use your favorite http testing tool to send a DELETE
request to http://localhost:8080/pacticipants/Zoo%20App
.
We publish our contract files using pact-node
, and we might also want to include it in our CI pipeline. Let’s create a bash script for that!
#!/usr/bin/env bash
#client/tasks/publish-pacts.sh
for f in pacts/*.json; do
consumer=$(jq '.consumer.name' $f | sed s'/"//g')
provider=$(jq '.provider.name' $f | sed s'/"//g')
consumer_version=$(jq '.version' package.json | sed s'/"//g')
curl -X PUT \-H "Content-Type: application/json" \
-d @$f \
http://localhost:8080/pacts/provider/$provider/consumer/$consumer/version/$consumer_version
done
This script iterates over all files in the pacts
directory, reads the consumer and provider name from the pact and the version of the consumer from its package.json
using jq, then sends a PUT
request to the broker with each pact file.
After that, we can check it out on the pact broker:
By clicking the little document in the middle, we can see this:
It will always show the latest uploaded pact. As you can see, it gives the providers the possibility to eyeball the data expected by the consumers, so we can even find out if we provide superfluous information or if we could get rid of endpoints that nobody uses.
We also get a nice call graph which is pretty simple at that point.
But it can be a lot more helpful later.
Now we have a way for the provider to check our contract against their API, so let’s get to it.
Example app – server side
We have the provider in place and it’s already taking requests, but we would want to make sure that it serves our current consumers the data they need. We have a simple Express app in place for this purpose.
// server/productService.js
const express = require('express')
const bodyParser = require('body-parser')
const controller = require('./controller')
const app = express()
app.use(bodyParser.json())
app.get('/', (req, res) => res.send('pact example server'))
app.get('/products', controller.get)
app.post('/products', controller.create)
app.get('/products/:id', controller.findById)
app.put('/products/:id', controller.updateById)
app.delete('/products/:id', controller.removeById)
module.exports = app
Currently we are only using the GET /products
endpoint in our consumer. The handlers can be found in our controller.js
file:
// server/controller.js
function get (req, res) {
res.json(products.getAll())
}
function create (req, res) {
const product = req.body
const savedProduct = products.create(product)
res.statusCode = 201
res.json(savedProduct)
}
And we still need a model to reach our database:
// server/model/products.js
const _ = require('lodash')
const data = new Map
// example record { id: 1, name: 'Cheap shoe', img: 'https://webshop.com/img/cheap-shoe.png' , price: 10, stock: 4 }
function getAll () {
return [...data.values()]
}
function create (product) {
const id = Math.max(...data.keys(), 0) + 1
data.set(id, Object.assign(product, { id }))
return data.get(id)
}
For the sake of simplicity we are not using any db in this example, just a simple Map
instance. Whenever a consumer requests all the data from the “db”, we return all the entries we have.
Step 4.: Creating the verifier script with pact-node
To test the contract, we need to set up the pact verifier first. We’ll be using [pact-node](https://github.com/pact-foundation/pact-node)
for verifying our pacts, because it’s documentation is better on the topic as pact-js
’s.
// server/consumerTests/verifyPacts.js
const pact = require('@pact-foundation/pact-node')
const path = require('path')
const opts = {
providerBaseUrl: 'http://localhost:3001', // where your service will be running during the test, either staging or localhost on CI
providerStatesSetupUrl: 'http://localhost:3001/test/setup', // the url to call to set up states
pactUrls: ['http://localhost:8080/pacts/provider/ProductService/consumer/Client/latest'] // the pacts to test against
}
pact.verifyPacts(opts).then(() => {
console.log('success')
process.exit(0)
}).catch((error) => {
console.log('failed', error)
process.exit(1)
})
And that’s it.
When we run this script, it will test our pacts against the running provider. As the product grows, you might need to add other pacts, or automate the addition of those, but the way you test them will remain essentially the same.
Step 5.: Adding a setup endpoint to our server
Let’s start the server for testing now.
Remember that when we set up the interaction, we defined the required state of our mock server. Now we need to provide a way so our actual provider can be in the state specified by the consumer. Pact will call POST /test/setup
as we set it up in the previous snippet. We will use the create
function we defined earlier to set the state as needed.
// server/consumerTests/testProductsService.js
const app = require('../productService')
const products = require('../model/products')
const port = process.env.PORT || 3001
app.post('/test/setup', (req, res) => {
const state = req.body.state
switch (state) {
case 'it has one product':
products.create({ name: 'Foo', img: 'https://webshop.com/img/foo.png', price: 1, stock: 1})
break
default:
break
}
res.end()
})
app.listen(port, (err) => {
if (err) {
throw err
}
console.log('SERVER: ProductService listening at', port)
})
And we’re good to go. We can see in the broker that the pact is verified.
Stay tuned for Part 2.
This week we saw how to use pact to test the boundaries between services. We saw how to create a mock server with pact for our client side unit tests, gathered those with Pact Broker and verified them against our running server making sure that the consumer and provider are on the same page.
We will release the second part of this article next week. In the upcoming episode, we will check how you can use pattern matching and query params for more complex use cases.
Update: The second part of the article is live on our blog! Click to read how to do Advanced Contract Testing with Pattern Matching.