In my article Semantic Javascript Testing with Jest, I proposed ways to reduce test verbiage by using Jest snapshots with property matchers. It was rather abstract, so I'm following it up with a more concrete example. In this article, I attempt to emulate Pact using Jest .
Pact is a testing tool that asserts that messages between a consumer and a provider conform to a contract. The contract defines the parameters: state
, uponReceiving
, withRequest
, and willRespondWith
. It is consumer driven, initially generated with a mock provider, and then validated against the real one.
Jest comes bundled with common serialisers for JSON, HTML, among others. But it can additionally serialise arbitrary payloads. In emulating Pact, I'll be using axios to make HTTP calls, so I'll setup Jest to serialize the relevant parts of the axios payloads – ie, by simply storing the config
, status
, headers
, and data
parts of the response . By converting a complex Javascript object into a simpler JSON form, highlighting its key parameters, not only can it be easily represented in textual format, but it can also be asserted against.
expect.addSnapshotSerializer({
test (value) {
/* serialize only axios responses */
return value &&
Object.prototype.hasOwnProperty.call(value, "request") &&
Object.prototype.hasOwnProperty.call(value, "config") &&
Object.prototype.hasOwnProperty.call(value, "status") &&
Object.prototype.hasOwnProperty.call(value, "headers") &&
Object.prototype.hasOwnProperty.call(value, "data")
},
print (value, serializer) {
/* serialize only key parameters */
const { config: { url, method }, status, headers, data } = value
return serializer({
config: { url, method },
status,
headers,
data
})
}
})
Using the Order API example in Pact's getting started guide , we can describe a contract in Jest below. Ignoring the superficial, this looks exactly the same! Notice the property matcher assertions, particularly in headers
and willRespondWith
. Not present in this example, but we could also have defined our own custom matchers , should the built-in ones prove insufficient.
const interactions = [{
state: "there are orders",
uponReceiving: "a request for orders",
withRequest: {
url: "/orders",
method: "GET"
},
willRespondWith: {
status: 200,
headers: expect.objectContaining({
"content-type": "application/json; charset=UTF-8"
}),
data: expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
items: expect.arrayContaining([
expect.objectContaining({
name: expect.any(String),
quantity: expect.any(Number),
value: expect.any(Number)
})
])
})
])
}
}]
const instance = axios.create({ baseURL })
interactions.forEach(({ state, uponReceiving, withRequest, willRespondWith }) => {
describe(state, () => {
it(uponReceiving, () =>
expect(instance(withRequest))
.resolves
.toMatchSnapshot(willRespondWith)
)
})
})
When executed, Jest automatically creates a snapshot file which describes the parameters of the consumer driven contract. It is ultimately just a multi-line string, but it satisfies the requirement that if the contract changes, the test will fail and highlight which parts of the contract has changed.
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`there are orders a request for orders 1`] = `
Object {
"config": Object {
"method": "get",
"url": "/orders",
},
"data": ArrayContaining [
ObjectContaining {
"id": Any<Number>,
"items": ArrayContaining [
ObjectContaining {
"name": Any<String>,
"quantity": Any<Number>,
"value": Any<Number>,
},
],
},
],
"headers": ObjectContaining {
"content-type": "application/json; charset=UTF-8",
},
"status": 200,
}
`;
So far, I've skipped over one important detail. Which provider was the above executed against? What is baseURL
?
When the provider has yet come to be, or when running local tests, the provider would be a mock one returning the desired responses, creating and updating the contract file as the test executes. When validating the actual provider, the same Jest test can provide validation that the contract is upheld.
If you have used Cypress , this is akin to setting CYPRESS_BASE_URL
.
Pact solves many more problems that the example above hasn't even begun to solve. For some tests, we need to set up provider states to ensure it has the correct data or configuration. When working across teams, the contract files need to be shared . If an API is being evolved, versioning is an issue.
If your needs are simple, and especially if you already are using Jest, maybe consider the above trick.