Emulating Pact With Jest Snapshots

Jest Snapshots are typically used for UI testing, but it can be much more. How about emulating the Pact workflow? 27 February 2020

# Background

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 .

# What is Pact?

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.

# Setup the serializer

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
})
}
})

# Define the contract

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)
)
})
})

# Generate the contract

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,
}
`
;

# Which provider?

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.

# Limitations

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.