Skip to content

Instantly share code, notes, and snippets.

@ValentinH
Last active March 11, 2023 21:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ValentinH/58514b558382a10a35dc50b7b7395fb9 to your computer and use it in GitHub Desktop.
Save ValentinH/58514b558382a10a35dc50b7b7395fb9 to your computer and use it in GitHub Desktop.
mockGQLResolver is a wrapper around MSW to mock any GQL operations
import { mockGQLResolver } from './mockGQLResolver';
describe('mockGQLResolver demo', () => {
it('mocking a query with a plain object', async () => {
mockGQLResolver({
queries: {
ExampleQueryName: {
users: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
},
},
});
// do your tests
});
it('mocking a mutation with a plain object and checking how it was called', async () => {
const receivers = mockGQLResolver({
mutations: {
ExampleMutationName: {
insert_users: {
affected_rows: 1,
},
},
},
});
// do your tests
expect(receivers.mutations.ExampleMutationName).toHaveBeenCalledTimes(1);
expect(receivers.mutations.ExampleMutationName).toHaveBeenCalledWith({
// this object contains the mutation variables
firstName: 'John',
});
// @ts-expect-error this is strongly typed as well
expect(receivers.mutations.WrongName).toHaveBeenCalledTimes(1);
});
it('mocking a subscription with a plain object', async () => {
mockGQLResolver({
subscriptions: {
ExampleSubName: {
users: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
},
},
});
// do your tests
});
it('mocking a query with different response based on call index or variables', async () => {
mockGQLResolver({
queries: {
// ctx, req and res are the same as in plain MSW `graphql.query`
ExampleQueryName: ({ callIndex, ctx, req, res }) => {
if (callIndex === 0) {
return {
users: [{ id: '1', name: 'John' }],
};
}
return {
users: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
};
},
},
});
// do your tests
});
it('you can also combine all of the above together', async () => {
const receivers = mockGQLResolver({
queries: {
ExampleQueryName1: {
users: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
},
ExampleQueryName2: {
organisations: [
{ id: '1', name: 'Apple' },
],
},
},
mutations: {
ExampleMutationName: {
insert_users: {
affected_rows: 1,
},
},
},
subscriptions: {
ExampleSubName: {
users: [
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' },
],
},
},
});
// do your tests
expect(receivers.queries.ExampleQueryName1).toHaveBeenCalledTimes(1);
expect(receivers.queries.ExampleQueryName2).toHaveBeenCalledTimes(1);
expect(receivers.mutations.ExampleMutationName).toHaveBeenCalledTimes(1);
});
});
import { graphql } from 'msw';
import { setupServer } from 'msw/node';
const mswServer = setupServer();
type QueryArgs = Parameters<Parameters<typeof graphql.query>[1]>;
type MutationArgs = Parameters<Parameters<typeof graphql.mutation>[1]>;
type SubscriptionArgs = Parameters<Parameters<typeof graphql.operation>[0]>;
type QueryFixtureFnArgs = {
callIndex: number;
req: QueryArgs[0];
res: QueryArgs[1];
ctx: QueryArgs[2];
};
type MutationFixtureFnArgs = {
callIndex: number;
req: MutationArgs[0];
res: MutationArgs[1];
ctx: MutationArgs[2];
};
type SubscriptionFixtureFnArgs = {
callIndex: number;
req: SubscriptionArgs[0];
res: SubscriptionArgs[1];
ctx: SubscriptionArgs[2];
};
/*
For this gist, I used `Record<string, any>` for these types.
In our real codebase, we are using here our types that are code-generated from our GraphQL schema.
Thanks to it, our mocks are type-safe.
*/
export type QueryFixture = Record<string, any>;
export type MutationFixture = Record<string, any>;
export type SubscriptionFixture = Record<string, any>;
export type QueryMocks = Record<
string,
QueryFixture | ((args: QueryFixtureFnArgs) => QueryFixture)
>;
export type MutationMocks = Record<
string,
MutationFixture | ((args: MutationFixtureFnArgs) => MutationFixture)
>;
export type SubscriptionMocks = Record<
string,
SubscriptionFixture | ((args: SubscriptionFixtureFnArgs) => SubscriptionFixture)
>;
export function mockGQLResolver<
Queries extends QueryMocks | undefined = QueryMocks,
Mutations extends MutationMocks | undefined = MutationMocks,
Subscriptions extends SubscriptionMocks | undefined = SubscriptionMocks,
QNames extends keyof NonNullable<Queries> = keyof NonNullable<Queries>,
MNames extends keyof NonNullable<Mutations> = keyof NonNullable<Mutations>,
SNames extends keyof NonNullable<Subscriptions> = keyof NonNullable<Subscriptions>,
QReceivers = Record<QNames, jest.Mock>,
MReceivers = Record<MNames, jest.Mock>,
SReceivers = Record<SNames, jest.Mock>
>(mocks: {
queries?: Queries;
mutations?: Mutations;
subscriptions?: Subscriptions;
}): {
queries: QReceivers;
mutations: MReceivers;
subscriptions: SReceivers;
} {
const queries = Object.entries(mocks.queries || {}) as [keyof QNames, Record<string, any>][];
const mutations = Object.entries(mocks.mutations || {}) as [keyof MNames, Record<string, any>][];
const subscriptions = Object.entries(mocks.subscriptions || {}) as [
keyof SNames,
Record<string, any>
][];
const queriesReceivers = queries.reduce<QReceivers>(
(acc, [qName]) => ({ ...acc, [qName]: jest.fn() }),
{} as QReceivers
);
const mutationsReceivers = mutations.reduce<MReceivers>(
(acc, [qName]) => ({ ...acc, [qName]: jest.fn() }),
{} as MReceivers
);
const subscriptionsReceivers = subscriptions.reduce<SReceivers>(
(acc, [qName]) => ({ ...acc, [qName]: jest.fn() }),
{} as SReceivers
);
mswServer.use(
...queries.map(([name, returnFixture]) =>
graphql.query(name.toString(), (req, res, ctx) => {
// @ts-expect-error For an unknown reason, there is no obvious way of making this
// type check pass. even force typing this as keyof QReceiver is not working
const callIndex = queriesReceivers[name].mock.calls.length;
// @ts-expect-error same issue as above
queriesReceivers[name](req.body?.variables);
const args: QueryFixtureFnArgs = { callIndex, req, res, ctx };
try {
const fixture = typeof returnFixture === 'function' ? returnFixture(args) : returnFixture;
return res(ctx.data(fixture));
} catch (e: any) {
if (e instanceof Error) {
throw e;
}
return res(ctx.errors(e));
}
})
),
...mutations.map(([name, returnFixture]) =>
graphql.mutation(name.toString(), (req, res, ctx) => {
// @ts-expect-error For an unknown reason, there is no obvious way of making this
// type check pass. even force typing this as keyof MReceiver is not working
const callIndex = mutationsReceivers[name].mock.calls.length;
// @ts-expect-error same issue as above
mutationsReceivers[name](req.body?.variables);
const args: MutationFixtureFnArgs = { callIndex, req, res, ctx };
try {
const fixture = typeof returnFixture === 'function' ? returnFixture(args) : returnFixture;
return res(ctx.data(fixture));
} catch (e: any) {
if (e instanceof Error) {
throw e;
}
return res(ctx.errors(e));
}
})
),
// MSW does not support subscriptions yet (https://github.com/mswjs/msw/issues/285)
// to workaround this, we can use `graphql.operation` to intercept any operation
// then we only handle the ones that have the right operation name
// else we return undefined which let the operation go through
graphql.operation((req, res, ctx) => {
if (!req.body || !('operationName' in req.body)) {
return;
}
const { operationName } = req.body;
const matchedSubscription = subscriptions.find((s) => s[0] === operationName);
if (!matchedSubscription) {
console.error(`Unhandled query/mutation "${operationName}". Please add a mock for it`);
return req.passthrough();
}
const [name, returnFixture] = matchedSubscription;
// @ts-expect-error For an unknown reason, there is no obvious way of making this
// type check pass. even force typing this as keyof SReceiver is not working
const callIndex = subscriptionsReceivers[name].mock.calls.length;
// @ts-expect-error same issue as above
subscriptionsReceivers[name](req.body?.variables);
const args: SubscriptionFixtureFnArgs = { callIndex, req, res, ctx };
try {
const fixture = typeof returnFixture === 'function' ? returnFixture(args) : returnFixture;
return res(ctx.data(fixture));
} catch (e: any) {
if (e instanceof Error) {
throw e;
}
return res(ctx.errors(e));
}
})
);
return {
queries: queriesReceivers,
mutations: mutationsReceivers,
subscriptions: subscriptionsReceivers,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment