Monthly Archives: August 2023

Adding PACT Contract Testing to an existing TypeScript code base

I like Contract Testing! I added a contract test with PACT-js and Jest for my consumer like this:

Installing PACT

  1. Disable the ignore-scripts setting: npm config set ignore-scripts false
  2. Ensure build chain is installed. Most linux based CI/CD agents have this out of the box. My local dev machine runs Windows; according to the installation guide for gyp the process is:
    1. Install Python from the MS App store. This takes about 5 minutes.
    2. Ensure the machine can build native code. My machine had Visual Studio already so I just added the ‘Desktop development with C++’ workload using the installer from ‘Tools -> Get Tools and Features’ This takes about 15 – 30 minutes
    3. npm install -g node-gyp
  3. Install the PACT js npmn package: npm i -S @pact-foundation/pact@latest
  4. Write a unit test using either the V3 or V2 of the PACT specification. See below for some examples.
  5. Update your CI build pipeline to publish the PACT like this: npx pact-broker publish ./pacts --consumer-app-version=$BUILD_BUILDNUMBER --auto-detect-version-properties --broker-base-url=$PACT_BROKER_BASE_URL --broker-token=$PACT_BROKER_TOKEN

A V3 version of a PACT unit test in Jest

//BffClient is the class implementing the logic to interact with the micro-service.
//the objective of this test is to:
//1. Define the PACT with the microservice
//2. Verify the class communicates according to the pact

import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import path from 'path';
import { BffClient } from './BffClient';

// Create a 'pact' between the two applications in the integration we are testing
const provider = new PactV3({
    dir: path.resolve(process.cwd(), 'pacts'),
    consumer: 'MyConsumer',
    provider: 'MyProvider',
});

describe('GET /', () => {
    it('returns OK and an array of items', () => {
        const exampleData: any = { name: "my-name", environment: "my-environment", };

        // Arrange: Setup our expected interactions. Pact mocks the microservice for us.
        provider
            .given('I have a list of items')
            .uponReceiving('a request for all items')
            .withRequest({method: 'GET', path: '/', })
            .willRespondWith({
                status: 200,
                headers: { 'Content-Type': 'application/json' },
                body: MatchersV3.eachLike(exampleData),
            });
        return provider.executeTest(async (mockserver) => {
            // Act: trigger our BffClient client code to do its behavior 
            // we configured it to use the mock instead of needing some external thing to run
            const sut = new BffClient(mockserver.url, "");
            const response = await sut.get()

            // Assert: check the result
            expect(response.status).toEqual(200)
            const data:any[] = await response.json()
            expect(data).toEqual([exampleData]);
        });
    });
});

A V2 version

import { Pact, Matchers } from '@pact-foundation/pact';
import path from 'path';
import { BffClient } from './BffClient';

// Create a 'pact' between the two applications in the integration we are testing
const provider = new Pact({
    dir: path.resolve(process.cwd(), 'pacts'),
    consumer: 'MyConsumer',
    provider: 'MyProvider',
});

describe('GET', () => {
    afterEach(() => provider.verify());
    afterAll(() => provider.finalize());

    it('returns OK and array of items', async () => {
        const exampleData: any = { name: "my-name", environment: "my-environment", };
        // Arrange: Setup our expected interactions. Pact mocks the microservice for us.
        await provider.setup()
        await provider.addInteraction({
            state: 'I have a list of items',
            uponReceiving: 'a request for all items',
            withRequest: { method: 'GET', path: '/',  },
            willRespondWith: {
                status: 200,
                headers: { 'Content-Type': 'application/json' },
                body: Matchers.eachLike(exampleData),
            },
        })

        // Act: trigger our BffClient client code to do its behavior 
        // we configured it to use the mock instead of needing some external thing to run
        const sut= new BffClient(mockserver.url, "");
        const response = sut.get()
        
        // Assert: check the result
        expect(response.status).toEqual(200)
        const data: any[] = await response.json()
        expect(data).toEqual([exampleData]);
    });
});