Tag Archives: typescript

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]);
    });
});
TypeScript logo

Property based testing in TypeScript with fast-check

In a previous post, I explained property based testing. In this post we’ll see a simple example using fast-check

Let assume we’re building the same bank transfer code as described in the dotnet FsCheck earlier post. Here’s the TypeScript version of the test:

import BuggyBank from "../buggy-bank"
import * as fc from 'fast-check';

function transferProperties(startBalanceA: bigint
        , startBalanceB: bigint
        , amount: bigint) : void {

    const bank = new BuggyBank()
    bank.balanceOfA = startBalanceA
    bank.balanceOfB = startBalanceB
    try {
        bank.transfer(amount)
    } catch {
        //Transfer failed
        const balanceAUnchanged = bank.balanceOfA == startBalanceA
        const balanceBUnchanged = bank.balanceOfB == startBalanceB
        expect(balanceAUnchanged).toBeTruthy()
        expect(balanceBUnchanged).toBeTruthy()
        return
    }
    //Transfer succeeded
    const balanceAIsNonNegative = bank.balanceOfA >= 0
    const balanceAChanged = bank.balanceOfA != startBalanceA
    const balanceBChanged = bank.balanceOfB != startBalanceB
    expect(balanceAIsNonNegative).toBeTruthy()
    expect(balanceAChanged).toBeTruthy()
    expect(balanceBChanged).toBeTruthy()
}

test('properties of a bank transfer must be correct', () => {
    const config = { numRuns : 10000 }
    //use this if you need to control the seed for the random number generator
    //const config = { numRuns : 10000, seed:123 }
    const property = fc.property(fc.bigIntN(32)
            , fc.bigIntN(32)
            , fc.bigIntN(32)
            , transferProperties)
    fc.assert(property,config)
})

I created the quick-n-dirty example like this:

mkdir fast-check-example
cd fast-check-example
npm init --yes
npm install typescript ts-node
echo "{}" > tsconfig.json
npm install --save-dev jest ts-jest @types/jest
npm install --save-dev fast-check
mkdir src
#Start vscode
code

The first time I ran the test, it detected a defect: The code allowed transferring zero amounts:

Property failed after 1 tests
{ seed: -1444529403, path: "0:2:0:0:1:0:5:1:3:0:0:1:1:0:1:2:2:0:0:0:0:0", endOnFailure: true }
Counterexample: [0n,0n,0n]
Shrunk 21 time(s)
...stack trace to relevant expect() line in code ....

After fixing that bug it detected another defect: Transfers succeed even when the source account’s balance is insufficient:

Property failed after 4 tests
{ seed: 1922422813, path: "3:0:0:1:0", endOnFailure: true }
Counterexample: [0n,0n,1n]
Shrunk 4 time(s)
...