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.
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.