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:
- The consumer writes a unit test of its behaviour using a Mock provided by Pact
- Pact writes the interactions into a contract file (as a JSON document)
- The consumer publishes the contract to a broker (or shares the file in some other way)
- Pact retrieves the contracts and replays the requests against a locally running provider
- 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.
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:
Consumer API
The Pact SDK uses a fluent builder to create interactions.
API | Options | Description |
---|---|---|
new PactV4(options) |
See constructor options below | Creates 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(...) |
V4UnconfiguredInteraction |
Start a builder for an HTTP interaction |
addSynchronousInteraction(...) |
V4UnconfiguredSynchronousMessage |
Start a builder for an asynchronous message |
| 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
Parameter | Required? | Type | Description |
---|---|---|---|
consumer |
yes | string | The name of the consumer |
provider |
yes | string | The name of the provider |
port |
no | number | The port to run the mock service on, defaults to a random machine assigned available port |
host |
no | string | The host to run the mock service, defaults to 127.0.0.1 |
tls |
no | boolean | flag to identify which protocol to be used (default false, HTTP) |
dir |
no | string | Directory to output pact files |
log |
no | string | File to log to |
logLevel |
no | string | Log level: one of 'trace', 'debug', 'info', 'error', 'fatal' or 'warn' |
spec |
no | number | Pact specification version (defaults to 2) |
The first step is to create a test for your API Consumer. The example below uses Mocha, and demonstrates the basic approach:
- Create the Pact object
- Start the Mock Provider that will stand in for your actual Provider
- Add the interactions you expect your consumer code to make when executing the tests
- 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.
- Validate the expected interactions were made between your consumer and the Mock Service
- 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
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.
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
.