• Stars
    star
    129
  • Rank 279,262 (Top 6 %)
  • Language
    JavaScript
  • Created almost 5 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Pact JS workshop - learn Pact in 60 minutes

Pact JS workshop

Introduction

This workshop is aimed at demonstrating core features and benefits of contract testing with Pact.

Whilst contract testing can be applied retrospectively to systems, we will follow the consumer driven contracts approach in this workshop - where a new consumer and provider are created in parallel to evolve a service over time, especially where there is some uncertainty with what is to be built.

This workshop should take from 1 to 2 hours, depending on how deep you want to go into each topic.

Workshop outline:

NOTE: Each step is tied to, and must be run within, a git branch, allowing you to progress through each stage incrementally. For example, to move to step 2 run the following: git checkout step2

Learning objectives

If running this as a team workshop format, you may want to take a look through the learning objectives.

Requirements

Docker

Docker Compose

Node + NPM

Scenario

There are two components in scope for our workshop.

  1. Product Catalog website. It provides an interface to query the Product service for product information.
  2. Product Service (Provider). Provides useful things about products, such as listing all products and getting the details of an individual product.

Step 1 - Simple Consumer calling Provider

We need to first create an HTTP client to make the calls to our provider service:

Simple Consumer

The Consumer has implemented the product service client which has the following:

  • GET /products - Retrieve all products
  • GET /products/{id} - Retrieve a single product by ID

The diagram below highlights the interaction for retrieving a product with ID 10:

Sequence Diagram

You can see the client interface we created in consumer/src/api.js:

export class API {

    constructor(url) {
        if (url === undefined || url === "") {
            url = process.env.REACT_APP_API_BASE_URL;
        }
        if (url.endsWith("/")) {
            url = url.substr(0, url.length - 1)
        }
        this.url = url
    }

    withPath(path) {
        if (!path.startsWith("/")) {
            path = "/" + path
        }
        return `${this.url}${path}`
    }

    async getAllProducts() {
        return axios.get(this.withPath("/products"))
            .then(r => r.data);
    }

    async getProduct(id) {
        return axios.get(this.withPath("/products/" + id))
            .then(r => r.data);
    }
}

After forking or cloning the repository, we may want to install the dependencies npm install. We can run the client with npm start --prefix consumer - it should fail with the error below, because the Provider is not running.

Failed step1 page

Move on to step 2

Step 2 - Client Tested but integration fails

Now lets create a basic test for our API client. We're going to check 2 things:

  1. That our client code hits the expected endpoint
  2. That the response is marshalled into an object that is usable, with the correct ID

You can see the client interface test we created in consumer/src/api.spec.js:

import API from "./api";
import nock from "nock";

describe("API", () => {

    test("get all products", async () => {
        const products = [
            {
                "id": "9",
                "type": "CREDIT_CARD",
                "name": "GEM Visa",
                "version": "v2"
            },
            {
                "id": "10",
                "type": "CREDIT_CARD",
                "name": "28 Degrees",
                "version": "v1"
            }
        ];
        nock(API.url)
            .get('/products')
            .reply(200,
                products,
                {'Access-Control-Allow-Origin': '*'});
        const respProducts = await API.getAllProducts();
        expect(respProducts).toEqual(products);
    });

    test("get product ID 50", async () => {
        const product = {
            "id": "50",
            "type": "CREDIT_CARD",
            "name": "28 Degrees",
            "version": "v1"
        };
        nock(API.url)
            .get('/products/50')
            .reply(200, product, {'Access-Control-Allow-Origin': '*'});
        const respProduct = await API.getProduct("50");
        expect(respProduct).toEqual(product);
    });
});

Unit Test With Mocked Response

Let's run this test and see it all pass:

❯ npm test --prefix consumer

PASS src/api.spec.js
  API
    βœ“ get all products (15ms)
    βœ“ get product ID 50 (3ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.03s
Ran all test suites.

If you encounter failing tests after running npm test --prefix consumer, make sure that the current branch is step2.

Meanwhile, our provider team has started building out their API in parallel. Let's run our website against our provider (you'll need two terminals to do this):

# Terminal 1
❯ npm start --prefix provider

Provider API listening on port 8080...
# Terminal 2
> npm start --prefix consumer

Compiled successfully!

You can now view pact-workshop-js in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://192.168.20.17:3000/

Note that the development build is not optimized.
To create a production build, use npm run build.

You should now see a screen showing 3 different products. There is a See more! button which should display detailed product information.

Let's see what happens!

Failed page

Doh! We are getting 404 everytime we try to view detailed product information. On closer inspection, the provider only knows about /product/{id} and /products.

We need to have a conversation about what the endpoint should be, but first...

Move on to step 3

Step 3 - Pact to the rescue

Unit tests are written and executed in isolation of any other services. When we write tests for code that talk to other services, they are built on trust that the contracts are upheld. There is no way to validate that the consumer and provider can communicate correctly.

An integration contract test is a test at the boundary of an external service verifying that it meets the contract expected by a consuming service β€” Martin Fowler

Adding contract tests via Pact would have highlighted the /product/{id} endpoint was incorrect.

Let us add Pact to the project and write a consumer pact test for the GET /products/{id} endpoint.

Provider states is an important concept of Pact that we need to introduce. These states help define the state that the provider should be in for specific interactions. For the moment, we will initially be testing the following states:

  • product with ID 10 exists
  • products exist

The consumer can define the state of an interaction using the given property.

Note how similar it looks to our unit test:

In consumer/src/api.pact.spec.js:

import path from "path";
import { PactV3, MatchersV3, SpecificationVersion, } from "@pact-foundation/pact";
import { API } from "./api";
const { eachLike, like } = MatchersV3;

const provider = new PactV3({
  consumer: "FrontendWebsite",
  provider: "ProductService",
  log: path.resolve(process.cwd(), "logs", "pact.log"),
  logLevel: "warn",
  dir: path.resolve(process.cwd(), "pacts"),
  spec: SpecificationVersion.SPECIFICATION_VERSION_V2,
});

describe("API Pact test", () => {
  describe("getting all products", () => {
    test("products exists", async () => {
      // set up Pact interactions
      await provider.addInteraction({
        states: [{ description: "products exist" }],
        uponReceiving: "get all products",
        withRequest: {
          method: "GET",
          path: "/products",
        },
        willRespondWith: {
          status: 200,
          headers: {
            "Content-Type": "application/json; charset=utf-8",
          },
          body: eachLike({
            id: "09",
            type: "CREDIT_CARD",
            name: "Gem Visa",
          }),
        },
      });

      await provider.executeTest(async (mockService) => {
        const api = new API(mockService.url);

        // make request to Pact mock server
        const product = await api.getAllProducts();

        expect(product).toStrictEqual([
          { id: "09", name: "Gem Visa", type: "CREDIT_CARD" },
        ]);
      });
    });
  });

  describe("getting one product", () => {
    test("ID 10 exists", async () => {
      // set up Pact interactions
      await provider.addInteraction({
        states: [{ description: "product with ID 10 exists" }],
        uponReceiving: "get product with ID 10",
        withRequest: {
          method: "GET",
          path: "/products/10",
        },
        willRespondWith: {
          status: 200,
          headers: {
            "Content-Type": "application/json; charset=utf-8",
          },
          body: like({
            id: "10",
            type: "CREDIT_CARD",
            name: "28 Degrees",
          }),
        },
      });

      await provider.executeTest(async (mockService) => {
        const api = new API(mockService.url);

        // make request to Pact mock server
        const product = await api.getProduct("10");

        expect(product).toStrictEqual({
          id: "10",
          type: "CREDIT_CARD",
          name: "28 Degrees",
        });
      });
    });
  });
});

Test using Pact

This test starts a mock server a random port that acts as our provider service. To get this to work we update the URL in the Client that we create, after initialising Pact.

To simplify running the tests, add this to consumer/package.json:

// add it under scripts
"test:pact": "CI=true react-scripts test --testTimeout 30000 pact.spec.js",

Running this test still passes, but it creates a pact file which we can use to validate our assumptions on the provider side, and have conversation around.

❯ npm run test:pact --prefix consumer

PASS src/api.spec.js
PASS src/api.pact.spec.js

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        2.792s, estimated 3s
Ran all test suites.

A pact file should have been generated in consumer/pacts/frontendwebsite-productservice.json

NOTE: even if the API client had been graciously provided for us by our Provider Team, it doesn't mean that we shouldn't write contract tests - because the version of the client we have may not always be in sync with the deployed API - and also because we will write tests on the output appropriate to our specific needs.

Move on to step 4

Step 4 - Verify the provider

We need to make the pact file (the contract) that was produced from the consumer test available to the Provider module. This will help us verify that the provider can meet the requirements as set out in the contract. For now, we'll hard code the path to where it is saved in the consumer test, in step 11 we investigate a better way of doing this.

Now let's make a start on writing Pact tests to validate the consumer contract:

In provider/product/product.pact.test.js:

const { Verifier } = require('@pact-foundation/pact');
const path = require('path');

// Setup provider server to verify
const app = require('express')();
app.use(require('./product.routes'));
const server = app.listen("8080");

describe("Pact Verification", () => {
    it("validates the expectations of ProductService", () => {
        const opts = {
            logLevel: "INFO",
            providerBaseUrl: "http://localhost:8080",
            provider: "ProductService",
            providerVersion: "1.0.0",
            pactUrls: [
                path.resolve(__dirname, '../../consumer/pacts/frontendwebsite-productservice.json')
            ]
        };

        return new Verifier(opts).verifyProvider().then(output => {
            console.log(output);
        }).finally(() => {
            server.close();
        });
    })
});

To simplify running the tests, add this to provider/package.json:

// add it under scripts
"test:pact": "npx jest --testTimeout=30000 --testMatch \"**/*.pact.test.js\""

We now need to validate the pact generated by the consumer is valid, by executing it against the running service provider, which should fail:

❯ npm run test:pact --prefix provider

Verifying a pact between FrontendWebsite and ProductService

  get product with ID 10
    returns a response which
      has status code 200 (FAILED)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (FAILED)
      has a matching body (FAILED)

  get all products
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)


Failures:

1) Verifying a pact between FrontendWebsite and ProductService Given product with ID 10 exists - get product with ID 10
    1.1) has a matching body
           expected 'application/json;charset=utf-8' body but was 'text/html;charset=utf-8'
    1.2) has status code 200
           expected 200 but was 404
    1.3) includes header 'Content-Type' with value 'application/json; charset=utf-8'
           Expected header 'Content-Type' to have value 'application/json; charset=utf-8' but was 'text/html; charset=utf-8'

Pact Verification

The test has failed, as the expected path /products/{id} is returning 404. We incorrectly believed our provider was following a RESTful design, but the authors were too lazy to implement a better routing solution πŸ€·πŸ»β€β™‚οΈ.

The correct endpoint which the consumer should call is /product/{id}.

Move on to step 5

Step 5 - Back to the client we go

We now need to update the consumer client and tests to hit the correct product path.

First, we need to update the GET route for the client:

In consumer/src/api.js:

async getProduct(id) {
  return axios.get(this.withPath("/product/" + id))
  .then(r => r.data);
}

Then we need to update the Pact test ID 10 exists to use the correct endpoint in path.

In consumer/src/api.pact.spec.js:

describe("getting one product", () => {
  test("ID 10 exists", async () => {

    // set up Pact interactions
    await provider.addInteraction({
      state: 'product with ID 10 exists',
      uponReceiving: 'get product with ID 10',
      withRequest: {
        method: 'GET',
        path: '/product/10'
      },

...

Pact Verification

Let's run and generate an updated pact file on the client:

❯ npm run test:pact --prefix consumer

PASS src/api.pact.spec.js
  API Pact test
    getting all products
      βœ“ products exists (18ms)
    getting one product
      βœ“ ID 10 exists (8ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.106s
Ran all test suites matching /pact.spec.js/i.

Now we run the provider tests again with the updated contract:

Run the command:

❯ npm run test:pact --prefix provider

Verifying a pact between FrontendWebsite and ProductService

  get product with ID 10
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

  get all products
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

Yay - green βœ…!

Move on to step 6

Step 6 - Consumer updates contract for missing products

We're now going to add 2 more scenarios for the contract

  • What happens when we make a call for a product that doesn't exist? We assume we'll get a 404.

  • What happens when we make a call for getting all products but none exist at the moment? We assume a 200 with an empty array.

Let's write a test for these scenarios, and then generate an updated pact file.

In consumer/src/api.pact.spec.js:

// within the 'getting all products' group
test("no products exists", async () => {

  // set up Pact interactions
  await provider.addInteraction({
    state: 'no products exist',
    uponReceiving: 'get all products',
    withRequest: {
      method: 'GET',
      path: '/products'
    },
    willRespondWith: {
      status: 200,
      headers: {
        'Content-Type': 'application/json; charset=utf-8'
      },
      body: []
    },
  });

  const api = new API(provider.mockService.baseUrl);

  // make request to Pact mock server
  const product = await api.getAllProducts();

  expect(product).toStrictEqual([]);
});

// within the 'getting one product' group
test("product does not exist", async () => {

  // set up Pact interactions
  await provider.addInteraction({
    state: 'product with ID 11 does not exist',
    uponReceiving: 'get product with ID 11',
    withRequest: {
      method: 'GET',
      path: '/product/11'
    },
    willRespondWith: {
      status: 404
    },
  });

  await provider.executeTest(async (mockService) => {
    const api = new API(mockService.url);

    // make request to Pact mock server
    await expect(api.getProduct("11")).rejects.toThrow(
    "Request failed with status code 404"
    );
  });
});

Notice that our new tests look almost identical to our previous tests, and only differ on the expectations of the response - the HTTP request expectations are exactly the same.

❯ npm run test:pact --prefix consumer

PASS src/api.pact.spec.js
  API Pact test
    getting all products
      βœ“ products exists (24ms)
      βœ“ no products exists (13ms)
    getting one product
      βœ“ ID 10 exists (14ms)
      βœ“ product does not exist (14ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        2.437s, estimated 3s
Ran all test suites matching /pact.spec.js/i.

What does our provider have to say about this new test:

❯ npm run test:pact --prefix provider

Verifying a pact between FrontendWebsite and ProductService

  get all products
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (FAILED)

  get product with ID 10
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

  get product with ID 11
    returns a response which
      has status code 404 (FAILED)
      has a matching body (OK)

  get all products
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)


Failures:

1) Verifying a pact between FrontendWebsite and ProductService Given no products exist - get all products
    1.1) has a matching body
           $ -> Expected an empty List but received [{"id":"09","name":"Gem Visa","type":"CREDIT_CARD","version":"v1"},{"id":"10","name":"28 Degrees","type":"CREDIT_CARD","version":"v1"},{"id":"11","name":"MyFlexiPay","type":"PERSONAL_LOAN","version":"v2"}]
2) Verifying a pact between FrontendWebsite and ProductService Given product with ID 11 does not exist - get product with ID 11
    2.1) has status code 404
           expected 404 but was 200

We expected this failure, because the product we are requesing does in fact exist! What we want to test for, is what happens if there is a different state on the Provider. This is what is referred to as "Provider states", and how Pact gets around test ordering and related issues.

We could resolve this by updating our consumer test to use a known non-existent product, but it's worth understanding how Provider states work more generally.

Move on to step 7

Step 7 - Adding the missing states

Our code already deals with missing users and sends a 404 response, however our test data fixture always has product ID 10 and 11 in our database.

In this step, we will add a state handler (stateHandlers) to our provider Pact verifications, which will update the state of our data store depending on which states the consumers require.

States are invoked prior to the actual test function is invoked. You can see the full lifecycle here.

We're going to add handlers for all our states:

  • products exist
  • no products exist
  • product with ID 10 exists
  • product with ID 11 does not exist

Let's open up our provider Pact verifications in provider/product/product.pact.test.js:

// add this to the Verifier opts
stateHandlers: {
  "product with ID 10 exists": () => {
    controller.repository.products = new Map([
      ["10", new Product("10", "CREDIT_CARD", "28 Degrees", "v1")]
    ]);
  },
  "products exist": () => {
    controller.repository.products = new Map([
      ["09", new Product("09", "CREDIT_CARD", "Gem Visa", "v1")],
      ["10", new Product("10", "CREDIT_CARD", "28 Degrees", "v1")]
    ]);
  },
  "no products exist": () => {
    controller.repository.products = new Map();
  },
  "product with ID 11 does not exist": () => {
    controller.repository.products = new Map();
  },
}

Let's see how we go now:

❯ npm run test:pact --prefix provider

Verifying a pact between FrontendWebsite and ProductService

  get all products
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

  get product with ID 10
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

  get product with ID 11
    returns a response which
      has status code 404 (OK)
      has a matching body (OK)

  get all products
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

NOTE: The states are not necessarily a 1 to 1 mapping with the consumer contract tests. You can reuse states amongst different tests. In this scenario we could have used no products exist for both tests which would have equally been valid.

Move on to step 8

Step 8 - Authorization

It turns out that not everyone should be able to use the API. After a discussion with the team, it was decided that a time-bound bearer token would suffice. The token must be in yyyy-MM-ddTHHmm format and within 1 hour of the current time.

In the case a valid bearer token is not provided, we expect a 401. Let's update the consumer to pass the bearer token, and capture this new 401 scenario.

In consumer/src/api.js:

    generateAuthToken() {
        return "Bearer " + new Date().toISOString()
    }

    async getAllProducts() {
        return axios.get(this.withPath("/products"), {
            headers: {
                "Authorization": this.generateAuthToken()
            }
        })
            .then(r => r.data);
    }

    async getProduct(id) {
        return axios.get(this.withPath("/product/" + id), {
            headers: {
                "Authorization": this.generateAuthToken()
            }
        })
            .then(r => r.data);
    }

In consumer/src/api.pact.spec.js we add authentication headers to the request setup for the existing tests:

      await provider.addInteraction({
        states: [{ description: "no products exist" }],
        uponReceiving: "get all products",
        withRequest: {
          method: "GET",
          path: "/products",
          headers: {
            Authorization: like("Bearer 2019-01-14T11:34:18.045Z"),
          },
        },
        willRespondWith: {
          status: 200,
          headers: {
            "Content-Type": "application/json; charset=utf-8",
          },
          body: [],
        },
      });

and we also add two new tests for the "no auth token" use case:

    // ...
    test("no auth token", async () => {

      // set up Pact interactions
      await provider.addInteraction({
        states: [{ description: "product with ID 10 exists" }],
        uponReceiving: "get product by ID 10 with no auth token",
        withRequest: {
          method: "GET",
          path: "/product/10",
        },
        willRespondWith: {
          status: 401,
        },
      });

      await provider.executeTest(async (mockService) => {
        const api = new API(mockService.url);

        // make request to Pact mock server
        await expect(api.getProduct("10")).rejects.toThrow(
          "Request failed with status code 401"
        );
      });
    });

Generate a new Pact file:

❯ npm run test:pact --prefix consumer

PASS src/api.pact.spec.js
  API Pact test
    getting all products
      βœ“ products exists (23ms)
      βœ“ no products exists (13ms)
      βœ“ no auth token (14ms)
    getting one product
      βœ“ ID 10 exists (12ms)
      βœ“ product does not exist (12ms)
      βœ“ no auth token (14ms)

Test Suites: 1 passed, 1 total
Tests:       6 passed, 6 total
Snapshots:   0 total
Time:        2.469s, estimated 3s
Ran all test suites matching /pact.spec.js/i.

We should now have two new interactions in our pact file.

Let's test the provider:

❯ npm run test:pact --prefix provider

Verifying a pact between FrontendWebsite and ProductService

  get all products
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

  get product by ID 10 with no auth token
    returns a response which
      has status code 401 (FAILED)
      has a matching body (OK)

  get product with ID 10
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

  get product with ID 11
    returns a response which
      has status code 404 (OK)
      has a matching body (OK)

  get all products
    returns a response which
      has status code 401 (FAILED)
      has a matching body (OK)


Failures:

1) Verifying a pact between FrontendWebsite and ProductService Given product with ID 10 exists - get product by ID 10 with no auth token
    1.1) has status code 401
           expected 401 but was 200
2) Verifying a pact between FrontendWebsite and ProductService Given products exist - get all products
    2.1) has status code 401
           expected 401 but was 200

Now with the most recently added interactions where we are expecting a response of 401 when no authorization header is sent, we are getting 200...

Move on to step 9*

Step 9 - Implement authorisation on the provider

We will add a middleware to check the Authorization header and deny the request with 401 if the token is older than 1 hour.

In provider/middleware/auth.middleware.js

// 'Token' should be a valid ISO 8601 timestamp within the last hour
const isValidAuthTimestamp = (timestamp) => {
    let diff = (new Date() - new Date(timestamp)) / 1000;
    return diff >= 0 && diff <= 3600
};

const authMiddleware = (req, res, next) => {
    if (!req.headers.authorization) {
        return res.status(401).json({ error: "Unauthorized" });
    }
    const timestamp = req.headers.authorization.replace("Bearer ", "")
    if (!isValidAuthTimestamp(timestamp)) {
        return res.status(401).json({ error: "Unauthorized" });
    }
    next();
};

module.exports = authMiddleware;

In provider/server.js

const authMiddleware = require('./middleware/auth.middleware');

// add this into your init function
app.use(authMiddleware);

We also need to add the middleware to the server our Pact tests use.

In provider/product/product.pact.test.js:

const authMiddleware = require('../middleware/auth.middleware');
app.use(authMiddleware);

This means that a client must present an HTTP Authorization header that looks as follows:

Authorization: Bearer 2006-01-02T15:04

Let's test this out:

❯ npm run test:pact --prefix provider

Verifying a pact between FrontendWebsite and ProductService

  get all products
    returns a response which
      has status code 200 (FAILED)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (FAILED)

  get product by ID 10 with no auth token
    returns a response which
      has status code 401 (OK)
      has a matching body (OK)

  get product with ID 10
    returns a response which
      has status code 200 (FAILED)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (FAILED)

  get product with ID 11
    returns a response which
      has status code 404 (FAILED)
      has a matching body (OK)

  get all products
    returns a response which
      has status code 401 (OK)
      has a matching body (OK)


Failures:

1) Verifying a pact between FrontendWebsite and ProductService Given no products exist - get all products
    1.1) has a matching body
           $ -> Type mismatch: Expected List [] but received Map {"error":"Unauthorized"}
    1.2) has status code 200
           expected 200 but was 401
2) Verifying a pact between FrontendWebsite and ProductService Given product with ID 10 exists - get product with ID 10
    2.1) has a matching body
           $ -> Actual map is missing the following keys: id, name, type
    2.2) has status code 200
           expected 200 but was 401
3) Verifying a pact between FrontendWebsite and ProductService Given product with ID 11 does not exist - get product with ID 11
    3.1) has status code 404
           expected 404 but was 401

There were 3 pact failures

Oh, dear. More tests are failing. Can you understand why?

Move on to step 10

Step 10 - Request Filters on the Provider

Because our pact file has static data in it, our bearer token is now out of date, so when Pact verification passes it to the Provider we get a 401. There are multiple ways to resolve this - mocking or stubbing out the authentication component is a common one. In our use case, we are going to use a process referred to as Request Filtering, using a RequestFilter.

NOTE: This is an advanced concept and should be used carefully, as it has the potential to invalidate a contract by bypassing its constraints. See https://github.com/DiUS/pact-jvm/blob/master/provider/junit/README.md#modifying-the-requests-before-they-are-sent for more details on this.

The approach we are going to take to inject the header is as follows:

  1. If we receive any Authorization header, we override the incoming request with a valid (in time) Authorization header, and continue with whatever call was being made
  2. If we don't receive an Authorization header, we do nothing

NOTE: We are not considering the 403 scenario in this example.

In provider/product/product.pact.test.js:

// add this to the Verifier opts
requestFilter: (req, res, next) => {
  if (!req.headers["authorization"]) {
    next();
    return;
  }
  req.headers["authorization"] = `Bearer ${ new Date().toISOString() }`;
  next();
},

We can now run the Provider tests

❯ npm run test:pact --prefix provider

Move on to step 11

Step 11 - Using a Pact Broker

Broker collaboration Workflow

We've been publishing our pacts from the consumer project by essentially sharing the file system with the provider. But this is not very manageable when you have multiple teams contributing to the code base, and pushing to CI. We can use a Pact Broker to do this instead.

Using a broker simplifies the management of pacts and adds a number of useful features, including some safety enhancements for continuous delivery which we'll see shortly.

In this workshop we will be using the open source Pact broker.

Running the Pact Broker with docker-compose

In the root directory, run:

docker-compose up

Publish contracts from consumer

First, in the consumer project we need to tell Pact about our broker. We can use the in built pact-broker CLI command to do this:

// add this under scripts
"pact:publish": "pact-broker publish ./pacts --consumer-app-version=\"$(npx --yes absolute-version)\" --auto-detect-version-properties --broker-base-url=http://localhost:8000 --broker-username pact_workshop --broker-password pact_workshop"

Now run

❯ npm run test:pact --prefix consumer

PASS src/api.pact.spec.js
  API Pact test
    getting all products
      βœ“ products exists (22ms)
      βœ“ no products exists (12ms)
      βœ“ no auth token (13ms)
    getting one product
      βœ“ ID 10 exists (11ms)
      βœ“ product does not exist (12ms)
      βœ“ no auth token (14ms)

Test Suites: 1 passed, 1 total
Tests:       6 passed, 6 total
Snapshots:   0 total
Time:        2.653s
Ran all test suites matching /pact.spec.js/i.

To publish the pacts:

❯ npm run pact:publish --prefix consumer

Created FrontendWebsite version 24c0e1-step11+24c0e1.SNAPSHOT.SB-AS-G7GM9F7 with branch step11
Pact successfully published for FrontendWebsite version 24c0e1-step11+24c0e1.SNAPSHOT.SB-AS-G7GM9F7 and provider ProductService.
  View the published pact at http://localhost:8000/pacts/provider/ProductService/consumer/FrontendWebsite/version/24c0e1-step11%2B24c0e1.SNAPSHOT.SB-AS-G7GM9F7
  Events detected: contract_published (pact content is the same as previous versions with tags  and no new tags were applied)
  Next steps:
    * Configure separate ProductService pact verification build and webhook to trigger it when the pact content changes. See https://docs.pact.io/go/webhooks

*NOTE: you would usually only publish pacts from CI. *

Have a browse around the broker on http://localhost:8000 (with username/password: pact_workshop/pact_workshop) and see your newly published contract!

Verify contracts on Provider

All we need to do for the provider is update where it finds its pacts, from local URLs, to one from a broker.

In provider/product/product.pact.test.js:

//replace
pactUrls: [
  path.resolve(__dirname, '../pacts/frontendwebsite-productservice.json')
],

// with
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL || "http://localhost:8000",
pactBrokerUsername: process.env.PACT_BROKER_USERNAME || "pact_workshop",
pactBrokerPassword: process.env.PACT_BROKER_PASSWORD || "pact_workshop",
// add to the opts {...}
publishVerificationResult: process.env.CI || process.env.PACT_BROKER_PUBLISH_VERIFICATION_RESULTS

Let's run the provider verification one last time after this change. It should print a few notices showing which pact(s) it has found from the broker, and why they were selected:

❯ PACT_BROKER_PUBLISH_VERIFICATION_RESULTS=true npm run test:pact --prefix provider

The pact at http://localhost:8000/pacts/provider/ProductService/consumer/FrontendWebsite/pact-version/80d8e7379fc7d5cfe503665ec1776bfb139aa8cf is being verified because the pact content belongs to the consumer version matching the following criterion:
    * latest version of FrontendWebsite that has a pact with ProductService (9cd950-step10+9cd950.SNAPSHOT.SB-AS-G7GM9F7)

Verifying a pact between FrontendWebsite and ProductService

  get all products
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

  get product by ID 10 with no auth token
    returns a response which
      has status code 401 (OK)
      has a matching body (OK)

  get product with ID 10
    returns a response which
      has status code 200 (OK)
      includes headers
        "Content-Type" with value "application/json; charset=utf-8" (OK)
      has a matching body (OK)

  get product with ID 11
    returns a response which
      has status code 404 (OK)
      has a matching body (OK)

  get all products
    returns a response which
      has status code 401 (OK)
      has a matching body (OK)

As part of this process, the results of the verification - the outcome (boolean) and the detailed information about the failures at the interaction level - are published to the Broker also.

This is one of the Broker's more powerful features. Referred to as Verifications, it allows providers to report back the status of a verification to the broker. You'll get a quick view of the status of each consumer and provider on a nice dashboard. But, it is much more important than this!

Can I deploy?

With just a simple use of the pact-broker can-i-deploy tool - the Broker will determine if a consumer or provider is safe to release to the specified environment.

You can run the pact-broker can-i-deploy checks as follows:

❯ npx pact-broker can-i-deploy \
               --pacticipant FrontendWebsite \
               --broker-base-url http://localhost:8000 \
               --broker-username pact_workshop \
               --broker-password pact_workshop \
               --latest

Computer says yes \o/

CONSUMER        | C.VERSION | PROVIDER       | P.VERSION | SUCCESS?
----------------|-----------|----------------|-----------|---------
FrontendWebsite | fe0b6a3   | ProductService | 1.0.0     | true

All required verification results are published and successful

----------------------------

❯ npx pact-broker can-i-deploy \
                --pacticipant ProductService \
                --broker-base-url http://localhost:8000 \
                --broker-username pact_workshop \
                --broker-password pact_workshop \
                --latest

Computer says yes \o/

CONSUMER        | C.VERSION | PROVIDER       | P.VERSION | SUCCESS?
----------------|-----------|----------------|-----------|---------
FrontendWebsite | fe0b6a3   | ProductService | 1.0.0     | true

All required verification results are published and successful

Step 12 - Using a PactFlow Broker

In step 11 we've been publishing our pacts from the consumer and provider projects to our locally hosted open source Pact broker.

We can use a managed Pact Broker from PactFlow to do this instead.

Using a hosted pact broker with pactflow, will allow you to concentrate on testing your application without having to worry about managing infrastructure, along with a number of other useful features.

Creating a pactflow account

Create a new PactFlow account and signup to the free Starter Plan. You will be emailed a set of credentials to access your account, these credentials are only for accessing the UI.

Grab your API Token(Click on settings -> API Tokens -> Read/write token -> COPY ENV VARS) and set the environment variables in your terminal as follows:

export PACT_BROKER_BASE_URL=https://<your_broker_name>.pactflow.io
export PACT_BROKER_TOKEN=exampleToken

Update your scripts to use the pact broker token based authentication method

First, in the consumer project we need to tell Pact about our broker.

In consumer/publish.pact.js:

const pact = require('@pact-foundation/pact-node');

if (!process.env.CI && !process.env.PUBLISH_PACT) {
    console.log("skipping Pact publish...");
    process.exit(0)
}

const pactBrokerUrl = process.env.PACT_BROKER_BASE_URL || 'https://<your_broker_name>.pactflow.io';
const pactBrokerToken = process.env.PACT_BROKER_TOKEN || 'pact_workshop';

const gitHash = require('child_process')
    .execSync('git rev-parse --short HEAD')
    .toString().trim();

const opts = {
    pactFilesOrDirs: ['./pacts/'],
    pactBroker: pactBrokerUrl,
    pactBrokerToken: pactBrokerToken,
    tags: ['prod', 'test'],
    consumerVersion: gitHash
};

pact
    .publishPacts(opts)
    .then(() => {
        console.log('Pact contract publishing complete!');
        console.log('');
        console.log(`Head over to ${pactBrokerUrl}`);
        console.log('to see your published contracts.')
    })
    .catch(e => {
        console.log('Pact contract publishing failed: ', e)
    });

Now run

❯ npm run test:pact --prefix consumer

> [email protected] test:pact /Users/you54f/dev/saf/dev/pact-workshop-clone/consumer
> react-scripts test --testTimeout 30000 pact.spec.js

PASS src/api.pact.spec.js
  API Pact test
    getting all products
      βœ“ products exists (19ms)
      βœ“ no products exists (10ms)
      βœ“ no auth token (10ms)
    getting one product
      βœ“ ID 10 exists (10ms)
      βœ“ product does not exist (8ms)
      βœ“ no auth token (12ms)

Then publish your pacts:

❯ npm run pact:publish --prefix consumer

> pact-broker publish ./pacts --consumer-app-version="$(npx --yes absolute-version)" --auto-detect-version-properties

Updated FrontendWebsite version 71c1b7-step12+71c1b7.SNAPSHOT.SB-AS-G7GM9F7 with branch step12
Pact successfully published for FrontendWebsite version 71c1b7-step12+71c1b7.SNAPSHOT.SB-AS-G7GM9F7 and provider ProductService.
  View the published pact at https://testdemo.pactflow.io/pacts/provider/ProductService/consumer/FrontendWebsite/version/71c1b7-step12%2B71c1b7.SNAPSHOT.SB-AS-G7GM9F7
  Events detected: contract_published, contract_requiring_verification_published, contract_content_changed (first time untagged pact published)
  Webhook "Automatically trigger pact verification on contract change." triggered for event contract_requiring_verification_published.
    View logs at https://testdemo.pactflow.io/triggered-webhooks/fa8d571e-8b61-41f8-9955-79a6fa9481fd/logs

Have a browse around your pactflow broker and see your newly published contract

Verify contracts on Provider

All we need to do for the provider is update where it finds its pacts, from local broker, to one from a hosted pactflow broker

In provider/product/product.pact.test.js:

//replace
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL || "http://localhost:8000",
pactBrokerUsername: process.env.PACT_BROKER_USERNAME || "pact_workshop",
pactBrokerPassword: process.env.PACT_BROKER_PASSWORD || "pact_workshop",

// with
pactBrokerUrl :process.env.PACT_BROKER_BASE_URL || 'https://<your_broker_name>.pactflow.io',
pactBrokerToken: process.env.PACT_BROKER_TOKEN || 'pact_workshop',

Let's run the provider verification one last time after this change:

❯ npm run test:pact --prefix provider

> [email protected] test:pact /Users/you54f/dev/saf/dev/pact-workshop-clone/provider
> npx jest --testTimeout 30000 --testMatch "**/*.pact.test.js"

INFO: [email protected]/84537 on safmac.local: Verifying provider
INFO: [email protected]/84537 on safmac.local: Verifying Pacts.
INFO: [email protected]/84537 on safmac.local: Verifying Pact Files
 PASS  product/product.pact.test.js (6.786s)
  Pact Verification
    βœ“ validates the expectations of ProductService (6006ms)
    INFO: Verification results published to https://you54f.pactflow.io/pacts/provider/ProductService/consumer/FrontendWebsite/pact-version/c4b62aae734255d00eba62ced76594343a148e29/verification-results/256

Can I deploy?

As per step 11, we can use the can-i-deploy command to gate releases.

You can run the pact-broker can-i-deploy checks as follows:

❯ pact-broker can-i-deploy \
               --pacticipant FrontendWebsite \
               --latest

Computer says yes \o/

CONSUMER        | C.VERSION | PROVIDER       | P.VERSION | SUCCESS?
----------------|-----------|----------------|-----------|---------
FrontendWebsite | fe0b6a3   | ProductService | 1.0.0     | true

All required verification results are published and successful

----------------------------

❯ pact-broker can-i-deploy \
                --pacticipant ProductService \
                --latest

Computer says yes \o/

CONSUMER        | C.VERSION | PROVIDER       | P.VERSION | SUCCESS?
----------------|-----------|----------------|-----------|---------
FrontendWebsite | fe0b6a3   | ProductService | 1.0.0     | true

All required verification results are published and successful

NOTE: Because we have exported the PACT_* environment variables, we can omit the necessary flags on the command.

That's it - you're now a Pact pro. Go build πŸ”¨

More Repositories

1

pact-ruby

Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
Ruby
2,160
star
2

pact-js

JS version of Pact. Pact is a contract testing framework for HTTP APIs and non-HTTP asynchronous messaging systems.
TypeScript
1,456
star
3

pact-jvm

JVM version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
Kotlin
1,082
star
4

pact-go

Golang version of Pact. Pact is a contract testing framework for HTTP APIs and non-HTTP asynchronous messaging systems.
Go
857
star
5

pact-net

.NET version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
C#
845
star
6

pact_broker

Enables your consumer driven contracts workflow
Ruby
702
star
7

pact-python

Python version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.
Python
501
star
8

pact-specification

Describes the pact format and verification specifications
295
star
9

pact-php

PHP version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project
PHP
244
star
10

pact.io

Pact Foundation Website
HTML
219
star
11

pact-js-core

Core binaries for pact-js, a Contract Testing Framework. NOTE: If you are looking to do Pact contract testing in node, you almost certainly want pact-js, not pact-node.
TypeScript
146
star
12

pact-workshop-go

Golang Pact workshop
Go
106
star
13

pact-broker-docker

Dockerized Pact Broker
Shell
92
star
14

pact-reference

Reference implementations for the pact specifications
Rust
87
star
15

pact-workshop-jvm-spring

Example Spring Boot project for the Pact workshop
Java
85
star
16

jest-pact

A Pact adaptor for to allow you to easily run tests with Jest
TypeScript
81
star
17

pact-workshop-dotnet-core-v1

A workshop for Pact using .NET Core
C#
76
star
18

pact-stub-server

Standalone pact stub server
Rust
75
star
19

pact-mock_service

Provides a mock service for use with Pact
Ruby
73
star
20

pact_broker-client

A Ruby and CLI client for the Pact Broker. Publish and retrieve pacts and verification results.
Ruby
69
star
21

nestjs-pact

Injectable Pact.js Consumer/Producer for NestJS
TypeScript
48
star
22

devrel

Developer Relations @ Pact - Your map to the Pact landscape for all-comers (maintainers, contributors, users, newbies)
38
star
23

pact-ruby-standalone

A standalone pact command line executable using the ruby pact implementation and Travelling Ruby
Shell
35
star
24

pact-provider-verifier

Cross-platform, generic language, Pact provider verification tool
Ruby
28
star
25

pact-workshop-Maven-Springboot-JUnit5

Pact Maven + Springboot + JUnit5 workshop - learn Pact in 60 minutes
28
star
26

pact-stub-server-archived

Wraps the Pact Rust mock server in a Docker container
Dockerfile
25
star
27

pact-js-mocha

EXPERIMENTAL Mocha Interface for Pact JS (warning: it is not recommended for beginners)
JavaScript
18
star
28

pact-plugins

🏰 Architecture to support Plugins πŸ”Œ with Pact πŸ”—
Rust
16
star
29

pact-provider-proxy

Allows pact verification against a running provider at a configurable base URL
Ruby
16
star
30

docs.pact.io

Pact documentation website
HTML
15
star
31

pact-broker-chart

This repository houses the Pact Broker Helm Chart
Smarty
11
star
32

karma-pact

Pact Framework Plugin for Karma
Shell
11
star
33

pact-5-minute-getting-started-guide

JavaScript
11
star
34

pact-ruby-cli

Amalgamated Pact Ruby CLI
Shell
11
star
35

pact-support

Shared code for Pact gems
Ruby
7
star
36

pact_broker-serverless

Pact Broker running in AWS Lambda with Serverless
Shell
7
star
37

pact-cplusplus

C++ DSL for Pact Library
HTML
6
star
38

pact-ruby-e2e-example

Code base to use for demonstrating features and recreating issues in the ruby implementation of pact. Please fork it and modify to recreate your own code.
Ruby
5
star
39

pact-xml

XML support for the Pact gem
Ruby
4
star
40

pact-plugin-template-golang

Pact πŸ”— Plugin πŸ”Œ template for the GoLang 🐿️ language = 🫢
Go
4
star
41

pact-message-demo

Ruby
4
star
42

pact-consumer-minitest

Minitest support for the Pact Consumer gem
Ruby
4
star
43

serverless-offline-pact

A serverless offline plugin to start one or more pact stub service alongside your serverless application
TypeScript
3
star
44

pact-mock-service-docker

Docker image running the Pact mock service
Ruby
3
star
45

homebrew-pact-ruby-standalone

The Pact Ruby Standalone public homebrew tap for macos/linux homebrew formulae
Shell
3
star
46

.github

The GitHub landing page for Pact - The de-facto contract testing tool
3
star
47

pact-event-bot

TypeScript
2
star
48

pact-standalone-npm

Pact Standalone wrapper for NPM projects
JavaScript
2
star
49

pact-parser

Small server to aid using existing pacts files as data sources in unit and integrational testing.
JavaScript
2
star
50

pact-mock-service-npm

Shell
2
star
51

pact-js-cli

The Broker CLI for Pact, but available to your node scripts
TypeScript
2
star
52

grunt-pact

A grunt task to run pact-node
JavaScript
1
star
53

pactr

R version of Pact. Enables consumer driven contract testing. Please read the Pact.io for specific information about PACT.
R
1
star
54

release-gem

Github action that bumps the version, generates the changelog, releases the gem, and creates a Github release
Shell
1
star
55

mocha-pact

An adaptor to allow you to easily run pact tests with Mocha
TypeScript
1
star
56

pact-ruby-standalone-e2e-example

Code base to use for demonstrating features and recreating issues in the Ruby standalone implementation of pact. Please fork it and modify to recreate your own code.
Shell
1
star
57

cypress-pact

Pact plugin for integrating Pact with Cypress tests
1
star
58

pact-js-dev-config

Shared configs for developers working on pact-js and related projects
JavaScript
1
star
59

blog.pact.io

Ghost application setup for blog.pact.io
SCSS
1
star