Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

pact

Package Overview
Dependencies
Maintainers
4
Versions
30
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pact

Pact for all things Javascript

  • 2.4.1
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
1.5K
decreased by-19.34%
Maintainers
4
Weekly downloads
 
Created
Source

Pact JS

Join the chat at https://gitter.im/realestate-com-au/pact

Build Status Code Climate Coverage Status Issue Count npm

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
APIOptionsReturnsDescription
pact(options)See Pact Node documentation for optionsObjectCreates 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/aPromiseStart the Mock Server
addInteraction()ObjectPromiseRegister 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/aPromiseVerifies that all interactions specified
finalize()n/aPromiseRecords the interactions registered to the Mock Server into the pact file and shuts it down.
removeInteractionsn/aPromiseIn 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:

  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)

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', () => {

  // (1) Create the Pact object to represent your provider
  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
  })

  // this is the response you expect from your Provider
  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) => {
        // (2) Start the mock server
        provider.setup()
          // (3) add interactions to the Mock Server, as many as required
          .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())
      })

      // (4) write your test(s)
      it('should generate a list of TODOs for the main screen', () => {
        const todoApp = new TodoApp();
        todoApp.getProjects() // <- this method would make the remote http call
          .then((projects) => {
      	    expect(projects).to.be.a('array')
            expect(projects).to.have.deep.property('projects[0].id', 1)

            // (5) validate the interactions occurred, this will throw an error if it fails telling you what went wrong
      	    return provider.verify()
          })
      })

      // (6) write the pact file for this consumer-provider pair,
      // and shutdown the associated mock server.
      // You should do this only _once_ per Provider you are testing.
      after(() => {
        provider.finalize()
      })
    })
  })
})

Splitting tests across multiple files

Pact tests tend to be quite long, due to the need to be specific about request/response payloads. Often times it is nicer to be able to split your tests across multiple files for manageability.

You have two options to achieve this feat:

  1. Create a Pact test helper to orchestrate the setup and teardown of the mock service for multiple tests.

    In larger test bases, this can significantly reduce test suite time and the amount of code you have to manage.

    See this example and this issue for more.

  2. Set pactfileWriteMode to update in the pact() constructor

    This will allow you to have multiple independent tests for a given Consumer-Provider pair, without it clobbering previous interactions.

    In larger test suites, you'll incur a slow down due to the time taken to start and stop the underlying mock servers.

    See this PR for background.

    NOTE: If using this approach, you must be careful to clear out existing pact files (e.g. rm ./pacts/*.json) before you run tests to ensure you don't have left over requests that are no longer relevent.

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:

APIOptionsReturnsDescription
verifyProvider()n/aPromiseStart the Mock Server
  1. Start your local Provider service.
  2. Optionally, instrument your API with ability to configure provider states
  3. Then run the Provider side verification step
const verifier = require('pact').Verifier;
let opts = {
	providerBaseUrl: <String>,            // Running API provider host endpoint. Required.
	pactBrokerUrl: <String>,              // URL of the Pact Broker to retrieve pacts from. Required if not using pactUrls.
	provider: <String>,                   // Name of the Provider. Required.
	tags: <Array>,                        // Array of tags, used to filter pacts from the Broker. Optional.
	pactUrls: <Array>,                    // Array of local Pact file paths or HTTP-based URLs (e.g. from a broker). Required if not using a Broker.
	providerStatesUrl: <String>,          // URL to fetch the provider states for the given provider API. Optional.
	providerStatesSetupUrl: <String>,     // URL to send PUT requests to setup a given provider state. Optional.
	pactBrokerUsername: <String>,         // Username for Pact Broker basic authentication. Optional
	pactBrokerPassword: <String>,         // Password for Pact Broker basic authentication. Optional
	publishVerificationResult: <Boolean>, // Publish verification result to Broker. Optional
	providerVersion: <Boolean>,           // Provider version, required to publish verification result to Broker. Optional otherwise.
	timeout: <Number>                     // The duration in ms we should wait to confirm verification process was successful. Defaults to 30000, Optional.
};

verifier.verifyProvider(opts)).then(function () {
	// do something
});

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>,               // Array of local Pact files or directories containing pact files. Path must be absolute. Required.
	pactBroker: <String>,            // URL to fetch the provider states for the given provider API. Optional.
	pactBrokerUsername: <String>,    // Username for Pact Broker basic authentication. Optional
	pactBrokerPassword: <String>,    // Password for Pact Broker basic authentication. Optional
	consumerVersion: <String>        // A string containing a semver-style version e.g. 1.0.0. Required.
};

pact.publishPacts(opts)).then(function () {
	// do something
});

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

// Define animal list payload, reusing existing object matcher
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

asciicast

Using Pact in non-Node environments

Pact requires a Node runtime to be able to start and stop Mock servers, write logs and other things.

However, when used within browser or non-Node based environments - such as with Karma or ng-test

  • this is not possible.

To address this challenge, we have released a separate 'web' based module for this purpose - pact-web. Whilst it still provides a testing DSL, it cannot start and stop mock servers as per the pact package, so you will need to coordinate this yourself prior to and after executing any tests.

To get started, install pact-web and Pact Node:

npm install --save-dev pact-web pact-node

If you're not using Karma, you can start and stop the mock server using Pact Node or something like Grunt Pact.

Using Pact with Karma

We have create a plugin for Karma, which will automatically start and stop any Mock Server for your Pact tests.

Modify your karma.conf.js file as per below to get started:

    // Load pact framework - this will start/stop mock server automatically
    frameworks: ['pact'],

    // load pact web
    files: [
      'node_modules/pact-web/pact-web.js',
      ...
    ]

Check out the Examples for how to use the Karma interface.

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

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. 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

Keywords

FAQs

Package last updated on 12 May 2017

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc