Skip to content

Instantly share code, notes, and snippets.

@quant-daddy
Created March 1, 2020 04:33
Show Gist options
  • Save quant-daddy/7f4b2891ef1963c484f8d9b80cbb5e9b to your computer and use it in GitHub Desktop.
Save quant-daddy/7f4b2891ef1963c484f8d9b80cbb5e9b to your computer and use it in GitHub Desktop.
How to do Pact contract testing between Apollo client and Graphql server

Pact Contract testing for GraphQL

Pact contract test consists of a consumer and a provider. In our case we are going to use a Apollo client consumer and the provider will a graphql server.

For the consumer testing, a graphql contract look like this:

// This is a apollo query; it can be mutation as well

const GET_USER_PROFILE = gql`
  query user($id: String!) {
    user(id: $id) {
      id
      email
      name
      avatarUrl
      __typename
    }
  }
`;

// Create the PactInteraction using a helper function
const getUserProfileInteraction = createGraphqlInteraction({
  query: GET_USER_PROFILE,
  variables: { id: userId },
  responseBody: {
    data: {
      user: {
        name: pact.Matchers.like("Some name"),
        avatarUrl: pact.Matchers.like(avatarUrl),
        id: userId,
        email: pact.Matchers.like(email),
        __typename: "User"
      }
    }
  },
  uponReceiving: "getUserProfile",
  state: `user with id ${userId} exists`,
  requestHeaders: { "x-auth": JSON.stringify({ userId }) }
});

where the function createGraphqlInteraction is

import { DocumentNode } from "graphql";
import { print } from "graphql/language/printer";
import * as pact from "@pact-foundation/pact";
import { GraphQLInteraction } from "@pact-foundation/pact";

export interface GraphQLInteractionObject {
  state: string;
  query: DocumentNode;
  variables?: { [key: string]: any };
  uponReceiving: string;
  requestHeaders?: { [key: string]: string };
  status?: number;
  responseBody: any;
}

const getOperation = (document: DocumentNode) =>
  document.definitions.find(a => a.kind === "OperationDefinition")?.name.value;

const contentTypeJsonMatcher = pact.Matchers.term({
  matcher: "application\\/json; *charset=utf-8",
  generate: "application/json; charset=utf-8"
});

export const createGraphqlInteraction = (
  params: GraphQLInteractionObject
): pact.GraphQLInteraction => {
  let result = new GraphQLInteraction()
    .uponReceiving(params.uponReceiving)
    .withQuery(print(params.query))
    .withOperation(getOperation(params.query))
    .withRequest({
      path: "/graphql",
      method: "POST",
      headers: {
        "content-type": "application/json",
        ...params.requestHeaders
      }
    })
    .willRespondWith({
      status: params.status || 200,
      headers: {
        "Content-Type": contentTypeJsonMatcher
      },
      body: params.responseBody
    })
    .given(params.state);
  if (params.variables) {
    result = result.withVariables(params.variables);
  }
  return result;
};

Note that since apollo client always includes the __typename field by default, we specify that field manually for each graphql query (otherwise the interaction object won't include that field and our test would break).

Next, we write our consumer test:

import { render } from "@testing-library/react";
import { pactWith } from "jest-pact";
import userEvent from "@testing-library/user-event";
import * as pact from "@pact-foundation/pact";

pactWith(
  { consumer: "Frontend", provider: "GroupService" },
  (provider: pact.Pact) => {
    let apolloClient: ApolloClient;
    beforeAll(() => {
      apolloClient = initApolloClient({
        graphqlURL: `${provider.mockService.baseUrl}/graphql`,
        headers: { "x-auth": JSON.stringify({ userId }) }
      });
    });
    describe("landing page integration", () => {
      test("renders", async () => {
        await provider.addInteraction(getUserProfileInteraction);
        const Wrapper = wrapperProvider(undefined, apolloClient);
        const { findByText } = render(
          <Wrapper>
            <Landing />
          </Wrapper>
        );
        await findByText(/some name/i);
      });
    });
  }
);

Points to note are:

  • We use pactWith library to create a provider.
  • We initialize the apolloClient in beforeAll to get the mockService url which the apolloclient will make request to.
  • Add the interaction right before the test.

The server side

Server side is simple. We need to start the server, add the state handlers of the pact contract and run verifyProvider. ONe gotha is that since all the test are run as one jest test, we need to reset our database everytime in the state handler. here's the code:

import { Verifier, VerifierOptions } from '@pact-foundation/pact';
import { App } from '#root/src/app';
import { resetDb } from './utils';
import config from '../config';
import { createUser } from '#root/src/utils/test/createUser';

const PORT = 8084;
const app = new App();

beforeAll(async () => {
  return app.start(PORT);
});

beforeEach(() => {
  return resetDb();
});

afterAll(async () => {
  return app.stop();
});

const stateHandlers: VerifierOptions['stateHandlers'] = {
  'user does not exist': async () => {
    console.log('nothing to do');
  },
  'user with id awkuhcfa87w4r exists': async () => {
    await resetDb();
    await createUser({ id: 'awkuhcfa87w4r' });
  },
};

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

const tags: string[] = [];
if (process.env.CI && process.env.BRANCH_NAME) {
  tags.push(process.env.BRANCH_NAME);
}

if (!process.env.CI) {
  tags.push('dev');
}
describe('Pact verification', () => {
  test('validates the expectations of GroupService', async () => {
    jest.setTimeout(30000);
    const logLevel: VerifierOptions['logLevel'] = 'info';
    const opts: VerifierOptions = {
      logLevel,
      providerBaseUrl: `http://localhost:${PORT}`,
      provider: 'GroupService',
      providerVersion: gitHash,
      consumerVersionTag: tags,
      providerVersionTag: tags,
      stateHandlers,
      pactBrokerUrl: config.PACT_BROKER_URL,
      pactBrokerToken: config.PACT_BROKER_TOKEN,
    };
    if (process.env.CI || process.env.PACT_PUBLISH_RESULTS) {
      Object.assign(opts, {
        publishVerificationResult: true,
      });
    }

    return new Verifier(opts).verifyProvider().finally(() => {
      app.stop();
    });
  });
});

Hope it helps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment