In previous posts, I have spoken about Pact.io. A wonderful set of tools, designed to help you and your team develop smarter, with consumer-driven contract tests.
We use Jest at work to test our TypeScript code, so it made sense to use Jest as our testing framework, to write our Pact unit tests with.
The Jest example on Pact-JS, involve a-lot of setup, which resulted in a fair bit of cognitive-load before a developer could start writing their Contract tests.
Inspired by a post by Tim Jones, one of the maintainers of Pact-JS and a member of the Dius team who built PACT, I decided to build and release an adapter for Jest, which would abstract the pact setup away from the developer, leaving them to concentrate on the tests.
Features
- instantiates the PactOptions for you
- Setups Pact mock service before and after hooks so you don’t have to
- Assign random ports and pass port back to user so we can run in parallel without port clashes
Adapter Installation
npm i jest-pact --save-dev OR yarn add jest-pact --dev
Usage
pactWith({ consumer: 'MyConsumer', provider: 'MyProvider' }, provider => { // regular pact tests go here }
Example
Say that your API layer looks something like this:
import axios from 'axios'; const defaultBaseUrl = "http://your-api.example.com" export const api = (baseUrl = defaultBaseUrl) => ({ getHealth: () => axios.get(`${baseUrl}/health`) .then(response => response.data.status) /* other endpoints here */ })
Then your test might look like:
import { pactWith } from 'jest-pact'; import { Matchers } from '@pact-foundation/pact'; import api from 'yourCode'; pactWith({ consumer: 'MyConsumer', provider: 'MyProvider' }, provider => { let client; beforeEach(() => { client = api(provider.mockService.baseUrl) }); describe('health endpoint', () => { // Here we set up the interaction that the Pact // mock provider will expect. // // jest-pact takes care of validating and tearing // down the provider for you. beforeEach(() => provider.addInteraction({ state: "Server is healthy", uponReceiving: 'A request for API health', willRespondWith: { status: 200, body: { status: Matchers.like('up'), }, }, withRequest: { method: 'GET', path: '/health', }, }) ); // You also test that the API returns the correct // response to the data layer. // // Although Pact will ensure that the provider // returned the expected object, you need to test that // your code recieves the right object. // // This is often the same as the object that was // in the network response, but (as illustrated // here) not always. it('returns server health', () => client.health().then(health => { expect(health).toEqual('up'); })); });
You can make your tests easier to read by extracting your request and responses:
/* pact.fixtures.js */ import { Matchers } from '@pact-foundation/pact'; export const healthRequest = { uponReceiving: 'A request for API health', withRequest: { method: 'GET', path: '/health', }, }; export const healthyResponse = { status: 200, body: { status: Matchers.like('up'), }, }
import { pactWith } from 'jest-pact'; import { healthRequest, healthyResponse } from "./pact.fixtures"; import api from 'yourCode'; pactWith({ consumer: 'MyConsumer', provider: 'MyProvider' }, provider => { let client; beforeEach(() => { client = api(provider.mockService.baseUrl) }); describe('health endpoint', () => { beforeEach(() => provider.addInteraction({ state: "Server is healthy", ...healthRequest, willRespondWith: healthyResponse }) ); it('returns server health', () => client.health().then(health => { expect(health).toEqual('up'); })); });
Configuration
pactWith(PactOptions, provider => { // regular pact tests go here } interface PactOptions { provider: string; consumer: string; port?: number; // defaults to a random port if not provided pactfileWriteMode?: PactFileWriteMode; dir? string // defaults to pact/pacts if not provided } type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; type PactFileWriteMode = "overwrite" | "update" | "merge";
Defaults
- Log files are written to /pact/logs
- Pact files are written to /pact/pacts
Jest Watch Mode
By default Jest will watch all your files for changes, which means it will run in an infinite loop as your pact tests will generate json pact files and log files.
You can get round this by using the following watchPathIgnorePatterns: ["pact/logs/*","pact/pacts/*"]
in your jest.config.js
Example
module.exports = { testMatch: ["**/*.test.(ts|js)", "**/*.it.(ts|js)", "**/*.pacttest.(ts|js)"], watchPathIgnorePatterns: ["pact/logs/*", "pact/pacts/*"] };
You can now run your tests with jest --watch
and when you change a pact file, or your source code, your pact tests will run
Examples of usage of jest-pact
See Jest-Pact-Typescript which showcases a full consumer workflow written in Typescript with Jest, using this adaptor
- Example pact tests
- AWS v4 Signed API Gateway Provider
- Soap API provider
- File upload API provider
- JSON API provider
Examples Installation
- clone repository
git@github.com:YOU54F/jest-pact-typescript.git
- Run
yarn install
- Run
yarn run pact-test
Generated pacts will be output in pact/pacts
Log files will be output in pact/logs