In the previous part of this article, we discussed how to perform consumer-driven contract testing with the Pact framework in a 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. 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... architecture. We created a mock server to perform client-side unit testing, collected these mocks into pacts and collected them with Pact Broker. Then we took this collection and verified it against our running server, making sure that the consumer and provider are on the same page.
To continue dealing with contact testing on a more advanced level, we are going to learn how to use pattern matching and query params for more complex use cases.
Why do we need pattern matching?
In our previous post, we tested a case where we knew that if all goes right the response we get during contract validation will be an exact match of our defined expectation. But for example, when we register a new entity, we usually don’t know the id which is going to be generated ahead of time so we cannot define an exact expectation on the whole returned object. In these cases, we can perform pattern matching to make sure the test won’t break on hard-coded values.
You can see the full capabilities of pattern matching here.
So let’s take a look at our example where we will further develop the ProductService
and Client
of the app we created last week, and we will make the system capable of registering new products!
Testing registration processes
We want to test if the registration process works, so we need to add that logic to the client as well.
// client/client.js
function registerProduct(product) {
return request.post({
url: `${PRODUCTS_SERVICE_URL}/products`,
body: product,
json: true,
headers: {
'Content-Type': 'application/json'
}
})
}
And here is our test case:
// client/client.spec.js
describe('#registerProduct', () => {
it('should send product registration request', async function () {
await provider.addInteraction(interactions.registerProduct)
const product = {
name: 'Bar',
img: 'https://webshop.com/img/cheap-shoe.png',
price: 2,
stock: 3
}
const response = await client.registerProduct(product)
expect(response).to.be.eql(Object.assign(product, { id: 1 }))
await provider.verify()
})
})
We only need to verify that the server was called, so the expectation could be omitted anyway. Calling provider.verify
only would be a sufficient method.
Pattern matching in interactions
In this example, we need to use the somethingLike
matcher. We can pass objects or primitive values, and the mock server will send the provided value as response to the consumer. Meanwhile, during validation, the matcher checks if the data sent by the provider matches the type we defined. This is how you can implement it in your code:
client/mockServer/interactions.js
const like = require('pact').Matchers.somethingLike
/* … */
const REGISTRATION_REQUEST_BODY = {
name: 'Bar',
img: 'https://webshop.com/img/cheap-shoe.png',
price: 2,
stock: 3
}
const REGISTRATION_RESPONSE_BODY = {
id: like(1),
name: 'Bar',
img: 'https://webshop.com/img/cheap-shoe.png',
price: 2,
stock: 3
}
module.exports = {
getProductList: { /* … */ },
registerProduct: {
state: 'it has one product',
uponReceiving: 'a request to create a new product',
withRequest: {
method: 'POST',
path: '/products',
body: REGISTRATION_REQUEST_BODY,
headers: {
'Content-Type': 'application/json'
}
},
willRespondWith: {
status: 201,
body: REGISTRATION_RESPONSE_BODY
}
}
}
Pact also has a term
matcher for writing regexes, but it can be tricky to use as terms are parsed in Ruby, so you may not always get the results you are expecting. Even worse, if you have some problems, you will have to understand the errors Ruby spews at you.
If you don’t expect the request body to be URL-encoded make sure to add the Content-Type
header as well.
After running the test, we only need to upload the pact file to the broker, and the provider can check if they return the necessary response.
Testing query params
The need arises to filter for a price when we retrieve the list of available products, so we need to use some query params as well. Let’s update the client side logic to make it possible.
// client/client.js
function getProducts (query) {
return request({
uri: `${PRODUCTS_SERVICE_URL}/products`,
qs: query,
json: true
})
}
Defining query params in interactions
Now let’s create the interaction.
//client/mockServer/interactions.js
const { somethingLike: like, eachLike, term } = require('pact').Matchers
const PRICE_FILTERED_PRODUCT_BODY = {
name: 'Foo',
img: 'foo-url',
price: 2
}
const PRICE_FILTERED_PRODUCT_QUERY = {
'min-price': '2',
'max-price': '5',
}
/* … */
module.exports = {
getProductList: { /* … */ },
getFilteredProductList: {
state: 'it has multiple products with different prices',
uponReceiving: 'a request to retrieve product list filtered by price',
withRequest: {
method: 'GET',
path: '/products',
query: PRICE_FILTERED_PRODUCT_QUERY
},
willRespondWith: {
status: 200,
body: eachLike(PRICE_FILTERED_PRODUCT_BODY)
}
},
registerProduct: { /* … */ }
}
We can provide the query params as an object, or if the order matters, we can pass an actual query string or a term
matcher as well. Be advised though, if you have to use matchers for the query params as well, they are parsed to first strings, so don’t use somethingLike
with a number in query objects.
The response body should be an array of objects, so we need to use the eachLike
matcher. The matcher asserts that all the objects in the array which were sent by the provider match the type of the object we defined.
Preparing the server for verification
We still need to make sure that the server will be in the correct state when we verify the pact. We’ll add a _flush
function to our db, so we can get rid of data created by previous tests.
SIDE NOTE: We handle cleanup this way only for the sake of simplicity, but it is definitely not the preferable way! Why? Because in case somebody makes a mistake and passes the address of the staging or production db to the test script, they might delete all our users’ data!
If we did the cleanup the right way, we would keep track of created entities and delete them by ID.
// server/model/products.js
/* … */
function _flush () {
data.clear()
}
/* … */
Verifying the Contracts
Now that we have a way to get rid of unnecessary products, let’s set up the state for the test:
// server/consumerTests/testProductsService.js
app.post('/test/setup', (req, res) => {
const state = req.body.state
switch (state) {
case 'it has one product':
products._flush()
products.create({ name: 'Foo', img: 'https://webshop.com/img/foo.png', price: 1, stock: 1})
break
case 'it has multiple products with different prices':
products._flush()
products.create({ name: 'Foo', img: 'https://webshop.com/img/foo.png', price: 1, stock: 1})
products.create({ name: 'Bar', img: 'https://webshop.com/img/bar.png', price: 2, stock: 3})
products.create({ name: 'Baz', img: 'https://webshop.com/img/baz.png', price: 3, stock: 5})
products.create({ name: 'Thing', img: 'https://webshop.com/img/thing.png', price: 6, stock: 2})
break
default:
break
}
res.end()
})
We also have to add another db function that will filter the products by prices for us:
// server/model/products.js
function getByPrice ({ minPrice = 0, maxPrice = Infinity }) {
const products = [...data.values()]
const productList = _.filter(products, (product) => product.price >= minPrice && product.price < maxPrice)
console.log(products)
return productList
}
And we also have to update our controller, so it will take query params into account:
// server/controller.js
function get (req, res) {
if (_.isEmpty(req.query)) {
return res.json(products.getAll())
}
const { 'min-price': minPrice, 'max-price': maxPrice } = req.query
return res.json(products.getByPrice({ minPrice, maxPrice }))
}
Now we can verify that our server will send back the necessary data and won’t break the client. However, as of right now we cannot use pact to verify that the data is filtered correctly as we cannot use matchers with numbers easily from JavaScript. We could convert all the numbers to strings and use a term
matcher to match them in the query param like that:
price: term({
matcher: '[2-5]', // match the provider’s response with this regex
generate: '2' // provide this number to the client
})
But in this case, we would have to stringify all our numbers and of course we don’t want to rewrite business logic just for the sake of testing.
UPDATE: as @mefellows kindly pointed out, this is more functional then contract testing, so you probably don’t want to do this anyway. Pact is only concerned about the structure of the API which should be verified without regard to the business logic, while functional tests of this API belong in the provider code base (the product search API service) and corresponding unit tests might exist on the consumer side to boundary test the client code.
Wrapping it up
Client-driven contract testing is a very powerful concept that we can not only use for verifying the safety of service boundaries, but also for designing and streamlining our APIs. Knowing what the needs of consumers are spares us a lot of guesswork while we are planning our tasks and writing our code. It is also easier and faster than setting up proper integration tests between services as we don’t need to have two live services communicating with each other.
You probably don’t want to break a CI task when a contract verification fails, because a typo in only one consumer’s mock could prevent you from rolling out a new release. However, it might help to quickly figure out why an error has occurred just by looking at the verification status of a contract.
Pact and Pact Broker are awesome tools for client driven contract testing, and they can be part of any developer’s toolbelt who works with distributed systems. If it had some more fine-grained assertion features, we could replace some test cases that currently can only be verified using complicated integration tests.
Unfortunately, complex use cases can be difficult to verify in Node.js. Why? Because matchers are evaluated in Ruby, therefore they often leak errors from the underlying runtime. Luckily, the Rust implementation of the underlying pact-standalone is underway, so hopefully, we will have better integrations soon.
The whole project is open source, so if you wish to contribute, you can head to their github page to find out where you can help out.