logo logo

Consumer Driven Contract Testing Using Pact.js

Consumer Driven Contract Testing Using Pact.js

In the previous article we discussed Microservices and the testing strategy which is Contract testing. In this chapter, we will look at how we can use PACT for consumer driven testing using JavaScript. 

Consumer Driven Contract Testing Using Pact.js

Table of Contents

  1. Introduction to Consumer Contract Testing
  2. You’re here → Consumer-Driven Contract Testing using Pact.js
  3. Consumer-Driven Contract Testing using Pact Java
  4. Consumer-Driven Contract Testing using Spring Cloud Contracts
  5. Event Driven Architecture: How to Perform Contract Testing in Kafka/pubSub
  6. Integrating Contract Testing in Build Pipelines

What is PACT?

Pact is a code-first tool for testing HTTP and message integrations using contract tests. Contract tests assert that inter-application messages conform to a shared understanding that is documented in a contract. Without contract testing, the only way to ensure that applications will work correctly together is by using expensive and brittle integration tests.

How Pact testing works?

A contract between a consumer and provider is called a pact. Each pact is a collection of interactions. Each interaction describes:

  • An expected request – Describing what the consumer is expected to send to the provider.
  • A minimal expected response – Describing the parts of the response the consumer wants the provider to return.

Who would typically implement Pact testing?

  • The consumer team is responsible for implementing the Pact tests in the consumer codebase that will generate the contract.
  • The provider team is responsible for setting up the Pact verification task in the provider codebase, and for writing the code that sets up the correct data for each provider state described in the contract.
  • Both teams are responsible for collaborating and communicating about the API and its usage!

Different Pact clients

There are various Pact clients’ and in this chapter with will use PACT for consumer driven testing using JavaScript.

pact-clients

To demonstrate the concept of Consumer Driven Contracts, we have the following back-end services: Date Provider – REST Service with the endpoint /provider/validDate validates whether the given date is valid or not.

Let’s get started 🚀

As we have the provider API which will return a valid response for a valid and invalid response for an incorrect date:

Valid Response

curl -w "%{http_code}" -X GET http://localhost:8081/provider/validDate\?date\=2020-11-11 | jq '.'                                                                             

{
  "test": "NO",
  "validDate": "2020-05-12T20:56:32+05:30",
  "count": 10
}
200

Invalid Response

curl -w "%{http_code}" -X GET http://localhost:8081/provider/validDate\?date\=2020 | jq '.'                                                                                   ~
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    32  100    32    0     0   2437      0 --:--:-- --:--:-- --:--:--  2461
{
  "error": "'2020' is not a date"
}
400

Let’s create a Consumer application which consumes the provider API and marshalls the response:

const request = require("superagent");
const API_HOST = "http://localhost";
const API_PORT = 9123;
const moment = require("moment");

const API_ENDPOINT = `${API_HOST}:${API_PORT}`;

class Consumer {
  async fetchDate(givenDate) {
    try {
      return await request.get(
        `${API_ENDPOINT}/provider/validDate?date=${givenDate}`
      );
    } catch (err) {
      return err.response;
    }
  }

  async parseDate(givenDate) {
    const response = await this.fetchDate(givenDate);
    if (moment(response.body.validDate).isValid()) {
      return {
        date: moment(givenDate, moment.ISO_8601).format("YYYY, LL"),
        format: "valid",
        expiry: "lifetime",
        count: response.body.count * 0.5
      };
    } else {
      throw new Error("Boom!! Invalid date format");
    }
  }
}

const consumer = new Consumer();
module.exports = { Consumer: consumer };

Let’s create a client that parses the given data:

const { Consumer } = require('./consumer');

Consumer.fetchDate('2020-11-11').then(
  response => {
    console.log(response);
  },
  error => {
    console.error(error);
  }
);

Now, when we run node client.js. The output would be:

Consumer using Provider API response

Great, we have a Consumer now using the Provider API response. Now the Consumer should make sure that the provider does not break the contract and this happens when the Consumer agrees to a contract with the Provider.

Consumer Side Testing

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

@pact-foundation/pact” 

const { Pact } = require(“@pact-foundation/pact”)

The Pact 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:

  • new Pact(options) – 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() – Start the Mock Server and wait for it to be available. You would normally call this only once in a beforeAll(…) type clause.
  • addInteractions() – Register an expectation on the Mock Server, which must be called by your test case(s). You can add multiple interactions per server, and each test would normally contain one or more of these. These will be validated and written to a pact if successful.
  • verify() – Verifies that all interactions specified. This should be called once per test, to ensure your expectations were correct.
  • finalize() – Records the interactions registered to the Mock Server into the pact file and shuts it down. You would normally call this only once in an afterAll(…) type clause.

Now let’s create a test for Consumer API. The example is using Mocha as the test runner.

  1. Create a PACT object which takes in different options like:
    1. consumer – The name of the Consumer.
    2. provider – The name of the Provider.
    3. pactfileWriteMode – Control how the Pact files are written. Choices: ‘overwrite’, ‘update’ or ‘none’. Defaults to ‘overwrite’.
    4. port  – The port to run the mock service on, defaults to 1234, in this example we are using port 9123.
  2. Start the Mock Provider that will stand in for your actual Provider, this will be implemented in the before hook.
  3. Add the interactions you expect your consumer code to make when executing the tests.
  4. Write your tests.
  5. Validate the expected interactions were made between your consumer and the Mock Service.
  6. Generate the pact(s):
const { Pact } = require('@pact-foundation/pact');
const { Consumer } = require('../consumer');
const path = require('path');
const chai = require('chai');
const nock = require('nock');
const interactions = require('../pact/interactions');
const API_PORT = 8081;

const API_HOST = `http://localhost:${API_PORT}`;
const expect = chai.expect;
const MOCK_SERVER_PORT = 9123;

describe('Pact Consumer', () => {
  const provider = new Pact({
    consumer: 'DateConsumer',
    provider: 'validDate provider',
    port: MOCK_SERVER_PORT,
    log: path.resolve(process.cwd(), 'logs', 'pact.log'),
    dir: path.resolve(process.cwd(), 'pacts'),
    logLevel: 'ERROR',
    pactfile_write_mode: 'update',
    spec: 1,
  });

  describe('Consumer Driven Contract', () => {
    before(() => {
      return provider.setup();
    });

    after(() => {
      return provider.finalize();
    });
    describe('When a call to date provider is made', () => {
      afterEach(() => {
        provider.removeInteractions();
      });

      describe('and valid date is provided', () => {
        it('can process the JSON payload from the date provider', async () => {
          provider.addInteraction(interactions.validDate);
          nock(API_HOST)
            .get('/provider/validDate?date=2020-11-11')
            .reply(200, {
              date: '2020, November 11, 2020',
              format: 'valid',
              expiry: 'lifetime',
              count: 5,
            });

          const response = await Consumer.parseDate('2020-11-11');
          expect(response).to.deep.equal({
            date: '2020, November 11, 2020',
            format: 'valid',
            expiry: 'lifetime',
            count: 2.5,
          });
        });
      });
      describe('and invalid date is provided', () => {
        it('Should thrown error when date param is null', async () => {
          provider.addInteraction(interactions.invalidDate);
          nock(API_HOST)
            .get("/provider/validDate?date=''")
            .reply(404, { error: 'validDate is required' });

          const response = await Consumer.parseDate('');
          expect(response.error).to.be.equal('validDate is required');
        });
      });
    });
  });
});

 

This will create the Pact file:

{
  "consumer": {
    "name": "DateConsumer"
  },
  "provider": {
    "name": "validDate provider"
  },
  "interactions": [
    {
      "description": "a request for JSON data",
      "providerState": "valid date",
      "request": {
        "method": "GET",
        "path": "/provider/validDate",
        "query": "date=2020-11-11"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json; charset=utf-8"
        },
        "body": {
          "test": "NO",
          "validDate": {
            "json_class": "Pact::Term",
            "data": {
              "generate": "2018-11-29T15:45:45+00:00",
              "matcher": {
                "json_class": "Regexp",
                "o": 0,
                "s": "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\+|\\-)\\d{2}:\\d{2}"
              }
            }
          },
          "count": {
            "json_class": "Pact::SomethingLike",
            "contents": 5
          }
        }
      }
    },
    {
      "description": "a request for JSON data",
      "providerState": "null date",
      "request": {
        "method": "GET",
        "path": "/provider/validDate",
        "query": "date="
      },
      "response": {
        "status": 404,
        "headers": {
          "Content-Type": "application/json; charset=utf-8"
        },
        "body": {
          "error": "validDate is required"
        }
      }
    }
  ],
  "metadata": {
    "pactSpecification": {
      "version": "1.0.0"
    }
  }
}

 

Below is a diagram demonstrating this process of consumer side testing (created by Pact JS core maintainer Matt Fellows):

Consumer Side Testing

Provider Side Testing

  1. Start your local Provider service.
  2. Optionally, instrument your API with the ability to configure provider states.
  3. Then run the Provider side verification step:
const { Verifier } = require('@pact-foundation/pact');
const { server } = require('../provider')
const path = require('path')

var opts = {
    providerBaseUrl: "http://localhost:8081",
    pactUrls: [
      path.resolve(__dirname, "../../../../pacts/dateconsumer-validdate_provider.json")
    ]
  };
  
describe('Pact Provider verification', () =>{
  it('Should validate the date consumer',async () =>{
    await new Verifier().verifyProvider(opts);
  })
})

Now when we run the provider test, it makes a request to the API which is mentioned in the Pact file and validates the response with the expected response from the Pact file, as also seen in the code and diagram below (created by Pact JS core maintainer Matt Fellows): 

Provider Side Testing

Provider Side Testing

Let’s say the Provider changes the Contract instead of returning error status code 404 for null date query param, they change to 400. Now when we run the provider test it should fail:

Provider Side Testing

Now the provider test failed and it calls out which consumer interaction is failing with the expected and actual values.

The entire working code snippets can be found here.

In the next chapter, we will see how to implement Pact with Java. Stay tuned 🙂

Happy Testing 😎
Sai Krishna & Srinivasan Sekar

About the author

Sai Krishna

Work at ThoughtWorks as Lead Consultant with 8 yrs of experience. Over the course of my career, I have worked on testing different mobile applications and building automation frameworks. Active contributor to Appium and also a member of Appium organization.

I love to contribute to open source technologies and passionate about new ways of thinking.

Leave a Reply

FacebookLinkedInTwitterEmail