Pact JS
Implementation of the consumer driven contract library Pact for Javascript.
From the Pact website:
The Pact family of frameworks provide support for Consumer Driven Contracts testing.
A Contract is a collection of agreements between a client (Consumer) and an API (Provider) that describes the interactions that can take place between them.
Consumer Driven Contracts is a pattern that drives the development of the Provider from its Consumers point of view.
Pact is a testing tool that guarantees those Contracts are satisfied.
Read Getting started with Pact for more information on
how to get going.
NOTE: This project supersedes Pact Consumer JS DSL.
Installation
It's easy, simply run the below:
npm install --save-dev pact
Using Pact JS
Using Mocha?
Check out Pact JS Mocha.
Consumer Side Testing
To use the library on your tests, add the pact dependency:
let Pact = require('pact')
The Pact
interface 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
API | Options | Returns | Description |
---|
pact(options) | See Pact Node documentation for options | Object | Creates a Mock Server test double of your Provider API. If you need multiple Providers for a scenario, you can create as many as these as you need. |
setup() | n/a | Promise | Start the Mock Server |
addInteraction() | Object | Promise | Register an expectation on the Mock Server, which must be called by your test case(s). You can add multiple interactions per server. These will be validated and written to a pact if successful. |
verify() | n/a | Promise | Verifies that all interactions specified |
finalize() | n/a | Promise | Records the interactions registered to the Mock Server into the pact file and shuts it down. |
removeInteractions | n/a | Promise | In some cases you might want to clear out the expectations of the Mock Service, call this to clear out any expectations for the next test run. NOTE: verify() will implicitly call this. |
Example
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)
Check out the examples
folder for examples with Karma Jasmine, Mocha and Jest. The example below is taken from the integration spec.
let path = require('path')
let chai = require('chai')
let pact = require('pact')
let request = require ('superagent')
let chaiAsPromised = require('chai-as-promised')
let expect = chai.expect
chai.use(chaiAsPromised);
describe('Pact', () => {
const provider = pact({
consumer: 'TodoApp',
provider: 'TodoService',,
port: MOCK_SERVER_PORT,
log: path.resolve(process.cwd(), 'logs', 'pact.log'),
dir: path.resolve(process.cwd(), 'pacts'),
logLevel: 'INFO',
spec: 2
})
const EXPECTED_BODY = [{
id: 1,
name: 'Project 1',
due: '2016-02-11T09:46:56.023Z',
tasks: [
{id: 1, name: 'Do the laundry', 'done': true},
{id: 2, name: 'Do the dishes', 'done': false},
{id: 3, name: 'Do the backyard', 'done': false},
{id: 4, name: 'Do nothing', 'done': false}
]
}]
context('when there are a list of projects', () => {
describe('and there is a valid user session', () => {
before((done) => {
provider.setup()
.then(() => {
provider.addInteraction({
state: 'i have a list of projects',
uponReceiving: 'a request for projects',
withRequest: {
method: 'GET',
path: '/projects',
headers: { 'Accept': 'application/json' }
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: EXPECTED_BODY
}
})
})
.then(() => done())
})
it('should generate a list of TODOs for the main screen', () => {
const todoApp = new TodoApp();
todoApp.getProjects()
.then((projects) => {
expect(projects).to.be.a('array')
expect(projects).to.have.deep.property('projects[0].id', 1)
return provider.verify()
})
})
after(() => {
provider.finalize()
})
})
})
})
Provider API Testing
Once you have created Pacts for your Consumer, you need to validate those Pacts against your Provider. The Verifier object provides the following API for you to do so:
API | Options | Returns | Description |
---|
verifyProvider() | n/a | Promise | Start the Mock Server |
- Start your local Provider service.
- Optionally, instrument your API with ability to configure provider states
- Then run the Provider side verification step
const verifier = require('pact').Verifier;
let opts = {
providerBaseUrl: <String>,
pactUrls: <Array>,
providerStatesUrl: <String>,
providerStatesSetupUrl <String>,
pactBrokerUsername: <String>,
pactBrokerPassword: <String>,
timeout: <Number>
};
verifier.verifyProvider(opts)).then(function () {
});
That's it! Read more about Verifying Pacts.
Publishing Pacts to a Broker
Sharing is caring - to simplify sharing Pacts between Consumers and Providers, checkout sharing pacts using the Pact Broker.
let pact = require('@pact-foundation/pact-node');
let opts = {
pactUrls: <Array>,
pactBroker: <String>,
pactBrokerUsername: <String>,
pactBrokerPassword: <String>,
consumerVersion: <String>
};
pact.publishPacts(opts)).then(function () {
});
Flexible Matching
Flexible matching makes your tests more expressive making your tests less brittle.
Rather than use hard-coded values which must then be present on the Provider side,
you can use regular expressions and type matches on objects and arrays to validate the
structure of your APIs.
Read more about using regular expressions and type based matching [here][https://github.com/realestate-com-au/pact/wiki/Regular-expressions-and-type-matching-with-Pact] before continuing.
NOTE: Make sure to start the mock service via the Pact
declaration with the option specification: 2
to get access to these features.
Match by regular expression
The underlying mock service is written in Ruby, so the regular expression must be in a Ruby format, not a Javascript format.
provider.addInteraction({
state: 'Has some animals',
uponReceiving: 'a request for an animal',
withRequest: {
method: 'GET',
path: '/animals/1'
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: {
id: 100,
name: "billy",
'gender': term({
matcher: 'F|M',
generate: 'F'
}),
}
}
})
Match based on type
provider.addInteraction({
state: 'Has some animals',
uponReceiving: 'a request for an animal',
withRequest: {
method: 'GET',
path: '/animals/1'
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: {
id: like(1),
name: like('Billy')
}
}
})
Match based on arrays
Matching provides the ability to specify flexible length arrays. For example:
pact.Matchers.eachLike(obj, { min: 3 })
Where obj
can be any javascript object, value or Pact.Match. It takes optional argument ({ min: 3 }
) where min is greater than 0 and defaults to 1 if not provided.
Below is an example that uses all of the Pact Matchers.
var somethingLike = pact.Matchers.somethingLike;
var term = pact.Matchers.term;
var eachLike = pact.Matchers.eachLike;
const animalBodyExpectation = {
'id': like(1),
'first_name': like('Billy'),
'last_name': like('Goat'),
'animal': like('goat'),
'age': like(21),
'gender': term({
matcher: 'F|M',
generate: 'M'
}),
'location': {
'description': like('Melbourne Zoo'),
'country': like('Australia'),
'post_code': like(3000)
},
'eligibility': {
'available': like(true),
'previously_married': like(false)
},
'interests': eachLike('walks in the garden/meadow')
}
const animalListExpectation = eachLike(animalBodyExpectation, {
min: MIN_ANIMALS
})
provider.addInteraction({
state: 'Has some animals',
uponReceiving: 'a request for all animals',
withRequest: {
method: 'GET',
path: '/animals/available'
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: animalListExpectation
}
})
Examples
Troubleshooting
If you are having issues, a good place to start is setting logLevel: 'DEBUG'
when configuring the pact({...})
object.
Timeout
Under the hood, Pact JS spins up a Ruby Mock Service.
On some systems, this may take more than a few seconds to start. It is recommended
to review your unit testing timeout to ensure it has sufficient time to start the server.
See here for more details.
Note on Jest
Jest uses JSDOM under the hood which may cause issues with libraries making HTTP request.
You'll need to add the following snippet to your package.json
to ensure it uses
the proper Node environment:
"jest": {
"testEnvironment": "node"
}
See this issue for background,
and the Jest example for a working example.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
If you would like to implement Pact
in another language, please check out the Pact specification and have a chat to one of us on the pact-dev Google group.
The vision is to have a compatible Pact
implementation in all the commonly used languages, your help would be greatly appreciated!
Contact