Skip to main content

Consumer Tests

Contract Testing Process (HTTP)​

Pact is a consumer-driven contract testing tool, which is a fancy way of saying that the API Consumer writes a test to set out its assumptions and needs of its API Provider(s). By unit testing our API client with Pact, it will produce a contract that we can share to our Provider to confirm these assumptions and prevent breaking changes.

The process looks like this:

diagram

  1. The consumer writes a unit test of its behaviour using a Mock provided by Pact
  2. Pact writes the interactions into a contract file (as a JSON document)
  3. The consumer publishes the contract to a broker (or shares the file in some other way)
  4. Pact retrieves the contracts and replays the requests against a locally running provider
  5. The provider should stub out its dependencies during a Pact test, to ensure tests are fast and more deterministic.

In this document, we will cover steps 1-3.

Consumer package​

To use the library on your tests, add the pact dependency:

const { PactV4 } = require("@pact-foundation/pact")

PactV4 is the latest version of this library, supporting up to and including version 4 of the Pact Specification. It also allows interactions of multiple types (HTTP, async, synchronous). For previous versions, see below.

Previous versions
const { Pact } = require("@pact-foundation/pact")   // Supports up to and including Pact Specification version 2
const { PactV3 } = require("@pact-foundation/pact") // Supportsu up to and including Pact Specification version 3

You should use the PactV4 interface unless you can't, and set the specification version via spec to the desired serialisation format.

The PactV4 class provides the following high-level APIs, they are listed in the order in which they typically get called in the lifecycle of testing a consumer:

API​

Consumer API

The Pact SDK uses a fluent builder to create interactions.

APIOptionsDescription
new PactV4(options)See constructor options belowCreates a Mock Server test double of your Provider API. The class is not thread safe, but you can run tests in parallel by creating as many instances as you need.
addInteraction(...)V4UnconfiguredInteractionStart a builder for an HTTP interaction
addSynchronousInteraction(...)V4UnconfiguredSynchronousMessageStart a builder for an asynchronous message

Common methods to builders​

| given(...) | Object | Set one or more provider states for the interaction | | uponReceiving(...) | string | The scenario name. The combination of given and uponReceiving must be unique in the pact file | | executeTest(...) | - | Executes a user defined function, passing in details of the dynamic mock service for use in the test. If successful, the pact file is updated. The function signature changes depending on the setup and context of the interaction. |

Constructor
ParameterRequired?TypeDescription
consumeryesstringThe name of the consumer
provideryesstringThe name of the provider
portnonumberThe port to run the mock service on, defaults to a random machine assigned available port
hostnostringThe host to run the mock service, defaults to 127.0.0.1
tlsnobooleanflag to identify which protocol to be used (default false, HTTP)
dirnostringDirectory to output pact files
lognostringFile to log to
logLevelnostringLog level: one of 'trace', 'debug', 'info', 'error', 'fatal' or 'warn'
specnonumberPact specification version (defaults to 2)

Example​

The first step is to create a test for your API Consumer. The example below uses Mocha, and demonstrates the basic approach:

  1. Create the Pact object
  2. Start the Mock Provider that will stand in for your actual Provider
  3. Add the interactions you expect your consumer code to make when executing the tests
  4. Write your tests - the important thing here is that you test the outbound collaborating function which calls the Provider, and not just issue raw http requests to the Provider. This ensures you are testing your actual running code, just like you would in any other unit test, and that the tests will always remain up to date with what your consumer is doing.
  5. Validate the expected interactions were made between your consumer and the Mock Service
  6. Generate the pact(s)

NOTE: you must also ensure you clear out your pact directory prior to running tests to ensure outdated interactions do not hang around

Check out the examples for more of these.

import { PactV4, MatchersV3 } from '@pact-foundation/pact';

// Create a 'pact' between the two applications in the integration we are testing
const provider = new PactV4({
dir: path.resolve(process.cwd(), 'pacts'),
consumer: 'MyConsumer',
provider: 'MyProvider',
spec: SpecificationVersion.SPECIFICATION_VERSION_V4, // Modify this as needed for your use case
});

// API Client that will fetch dogs from the Dog API
// This is the target of our Pact test
public getMeDogs = (from: string): AxiosPromise => {
return axios.request({
baseURL: this.url,
params: { from },
headers: { Accept: 'application/json' },
method: 'GET',
url: '/dogs',
});
};

const dogExample = { dog: 1 };
const EXPECTED_BODY = MatchersV3.eachLike(dogExample);

describe('GET /dogs', () => {
it('returns an HTTP 200 and a list of dogs', () => {
// Arrange: Setup our expected interactions
//
// We use Pact to mock out the backend API
provider
.addInteraction()
.given('I have a list of dogs')
.uponReceiving('a request for all dogs with the builder pattern')
.withRequest('GET', '/dogs' (builder) => {
builder.query({ from: 'today' })
builder.headers({ Accept: 'application/json' })
})
.willRespondWith(200, (builder) => {
builder.headers({ 'Content-Type': 'application/json' })
builder.jsonBody(EXPECTED_BODY)
});

return provider.executeTest((mockserver) => {
// Act: test our API client behaves correctly
//
// Note we configure the DogService API client dynamically to
// point to the mock service Pact created for us, instead of
// the real one
dogService = new DogService(mockserver.url);
const response = await dogService.getMeDogs('today')

// Assert: check the result
expect(response.data[0]).to.deep.eq(dogExample);
});
});
});

Read on about matching

Publishing Pacts to a Broker​

Sharing is caring - to simplify sharing Pacts between Consumers and Providers, we have created the Pact Broker.

The Broker:

  • versions your contracts
  • tells you which versions of your applications can be deployed safely together
  • allows you to deploy your services independently
  • provides API documentation of your applications that is guaranteed to be up-to date
  • visualises the relationships between your services
  • integrates with other systems, such as Slack or your CI server, via webhooks
  • ...and much much more.

Host your own using the open source docker image, or sign-up for a free hosted Pact Broker with our friends at PactFlow.

Publish in npm scripts​

The easiest way to publish pacts to the broker is via an npm script in your package.json:


"test:publish": "./node_modules/.bin/pact-broker publish <YOUR_PACT_FILES_OR_DIR> --consumer-app-version=\"$(npx absolute-version)\" --auto-detect-version-properties --broker-base-url=https://your-broker-url.example.com"

You'll need to install @pact-foundation/pact-cli package to use the pact-broker command. This is a standalone package that can be installed via npm.

For a full list of the options, see the CLI usage instructions.

All CLI binaries are available in npm scripts when using pact-js-cli @pact-foundation/pact-cli.

If you want to pass your username and password to the broker without including them in scripts, you can provide it via the environment variables PACT_BROKER_USERNAME and PACT_BROKER_PASSWORD. If your broker supports an access token instead of a password, use the environment variable PACT_BROKER_TOKEN.