graphql-query-test-mock
Version:
Mock queries to your GraphQL backend in your Jest tests.
304 lines (249 loc) • 8.66 kB
Flow
// @flow strict
import nock from 'nock';
import url from 'url';
import { defaultChangeServerResponseFn } from './defaults';
import { getNockRequestHandlerFn } from './getNockRequestHandlerFn';
import type { Data, ServerResponse, Variables, Error } from './types';
import { createMockGraphQLRecord, getQueryMockID } from './utils';
export type MockGraphQLConfig = {|
/**
* The name of the query to mock.
* Example: `query QueryNameIsHere { ... }`.
*/
name: string,
/**
* The data to return for this particular query.
*/
data: Data,
/**
* If you need to return GraphQL errors you can return an array of them here. Note that
* this is different to the "error" prop below, and this should be used whenever the
* GraphQL server would respond with a status 200, but have GraphQL errors.
*/
graphqlErrors?: Array<Error>,
/**
* If you want, you can return a custom error here that'll be thrown by the API if
* you return a status like 401. Note that this is *an error for the entire request*,
* not GraphQL errors. For returning GraphQL errors, see the "graphqlErrors" prop.
*/
error?: Data,
/**
* The status to return from the API. Defaults to 200, but can be changed to 401
* or similar if you want to test more complex interactions with the API.
*/
status?: number,
/**
* This tells QueryMock whether you want the query you mock to only be valid for the
* exact variables you pass `mockQuery` when mocking. Matching on the variables is
* a convenient way to test that the correct variables are used by your app.
* Default: true.
*/
matchOnVariables?: boolean,
/**
* This is a convenience method you can use if you want to match on variables in a
* more dynamic way, for example when using relative dates in your queries.
* NOTE: Use this only in very specific cases, instead prefer using "ignoreThesePropertiesInVariables"
* defined below.
*
* Takes precedence over matchOnVariables above if specified.
*/
matchVariables?: (variables: Variables) => boolean | Promise<boolean>,
/**
* The variables to match this specific query mock to.
* Example: variables: { id: 123 } would only match when exactly { id: 123 } is sent
* as variables for this query.
*/
variables?: Variables,
/**
* A list of properties to ignore when matching variables. This is very useful when you use unstable variables
* like dates in your queries. For example:
* ignoreThesePropertiesInVariables: ["fromDate", "toDate"] used with variables: { someProp: true, fromDate: someDate, toDate: someOtherDate }
* will match variables only on someProp, and ignore fromDate/toDate.
*/
ignoreThesePropertiesInVariables?: Array<string>,
/**
* Whether to persist this mock or not, meaning whether it should be valid for several
* calls in a row, or delete itself after being used once. Set this to false when you
* need to test a more complex flow of calls using the same query, but needing different
* responses.
*
* Default: true
*/
persist?: boolean,
/**
* A custom handler that lets you return a custom `nock` response for this mock.
* `req` is the `nock` request, and it expects you to return [statusCode, serverResponse], like:
* [200, { data: { id: '123 } }].
*/
customHandler?: (
req: *,
config: {|
query: string,
operationName: string,
variables: ?Variables
|}
) => [number, ServerResponse] | Promise<[number, ServerResponse]>,
/**
* Sometimes you need to change the server response object dynamically for a query mock.
* This allows you to do so. Example:
* changeServerResponse: (config, response) => ({ ...response, invalidToken: true })
*/
changeServerResponse?: ChangeServerResponseFn
|};
export type MockGraphQLRecord = {|
id: string,
queryMockConfig: MockGraphQLConfig,
resolveQueryPromise?: Promise<mixed>
|};
export type RecordedGraphQLQuery = {|
/**
* The id of the query. Same as the query name, ex: `query QueryName { ... }`.
*/
id: string,
/**
* Variables used in this specific call.
*/
variables: ?Variables,
/**
* Headers used for this specific call.
*/
headers: { [key: string]: string },
/**
* Full response object returned by the server for this specific call.
*/
response: ServerResponse
|};
type QueryStoreObj = {
[queryName: string]: Array<MockGraphQLRecord>
};
export type ChangeServerResponseFn = (
mockQueryConfig: MockGraphQLConfig,
serverResponse: ServerResponse
) => ServerResponse;
type CreateQueryMockConfig = {|
changeServerResponse?: ChangeServerResponseFn
|};
export class QueryMock {
_calls: Array<RecordedGraphQLQuery> = [];
_queries: QueryStoreObj = {};
_changeServerResponseFn: ChangeServerResponseFn = defaultChangeServerResponseFn;
constructor(config: ?CreateQueryMockConfig) {
if (!config) {
return;
}
const { changeServerResponse } = config;
if (changeServerResponse) {
this._changeServerResponseFn = changeServerResponse;
}
}
_addCall(call: RecordedGraphQLQuery) {
this._calls.push(call);
}
reset() {
this._calls = [];
this._queries = {};
}
getCalls(): Array<RecordedGraphQLQuery> {
return [...this._calls];
}
_getOrCreateMockQueryHolder(id: string): Array<MockGraphQLRecord> {
if (!this._queries[id]) {
this._queries[id] = [];
}
return this._queries[id];
}
mockQuery(config: MockGraphQLConfig) {
this._getOrCreateMockQueryHolder(config.name).push(
createMockGraphQLRecord(config)
);
}
mockQueryWithControlledResolution(config: MockGraphQLConfig): () => void {
let resolver = null;
const resolveQueryPromise = new Promise(resolve => {
resolver = resolve;
});
const resolveQueryFn = () => {
let interval = setInterval(() => {
if (resolver) {
clearInterval(interval);
resolver();
}
}, 50);
};
this._getOrCreateMockQueryHolder(config.name).push(
createMockGraphQLRecord(config, resolveQueryPromise)
);
return resolveQueryFn;
}
_getQueryMock(name: string, variables: ?Variables): ?MockGraphQLRecord {
const queryMockHolder = this._queries[name];
if (!queryMockHolder || queryMockHolder.length < 1) {
return null;
}
let matchingQueryMock: ?MockGraphQLRecord = null;
for (let i = 0; i <= queryMockHolder.length - 1; i += 1) {
const thisQueryMock = queryMockHolder[i];
const {
matchVariables,
matchOnVariables,
ignoreThesePropertiesInVariables
} = thisQueryMock.queryMockConfig;
// Use custom variables match function if it exists
if (matchVariables && matchVariables(variables || {})) {
matchingQueryMock = thisQueryMock;
break;
}
// Bail if this query mock is not configured to match on variables. We handle that case below after the loop.
if (!matchOnVariables) {
continue;
}
/**
* Get the ID of this particular mocked query using the provided mock's data.
* This enables us to check if this mock matches, accounting for any properties
* that are to be ignored in the mock variables.
*/
const processedIdForProvidedMockData = getQueryMockID(
name,
variables,
ignoreThesePropertiesInVariables || []
);
if (processedIdForProvidedMockData === thisQueryMock.id) {
matchingQueryMock = thisQueryMock;
break;
}
}
/**
* If we got all the way here it means we found no matches.
* We'll check if any of the mocked queries has variable matching off,
* and if so return that. We start from the latest added queries.
*/
for (let i = queryMockHolder.length - 1; i >= 0; i -= 1) {
const thisQueryMock = queryMockHolder[i];
if (!thisQueryMock.queryMockConfig.matchOnVariables) {
matchingQueryMock = thisQueryMock;
break;
}
}
if (matchingQueryMock) {
return matchingQueryMock.queryMockConfig.persist
? matchingQueryMock
: queryMockHolder
.splice(queryMockHolder.indexOf(matchingQueryMock), 1)
.pop();
}
return null;
}
setup(graphQLURL: string) {
this.reset();
const theUrl = url.parse(graphQLURL);
nock(`${theUrl.protocol || 'https:'}//${theUrl.host || 'localhost'}`)
.persist()
.post(theUrl.path || '/')
.reply(getNockRequestHandlerFn(this));
}
cleanup() {
nock.cleanAll();
nock.enableNetConnect();
nock.restore();
}
}