@wayfair/gqmock
: GQMock - GraphQL Mocking ServiceGraphqlMockingService
async GraphqlMockingService.start
async GraphqlMockingService.stop
async GraphqlMockingService.registerSchema
GraphqlMockingService.createContext
GraphqlMockingContext
GraphqlMockingContext.sequenceId
async GraphqlMockingContext.operation
async GraphqlMockingContext.networkError
http:localhost:<port>/graphql
http:localhost:<port>/graphql
http:localhost:<port>/schema/register
http:localhost:<port>/seed/operation
http:localhost:<port>/seed/network-error
There isn't an easy way to customize mock data coming from Apollo Server without writing logic directly into the mock service implementation. This requires understanding the mock server templating implementation and hand rolling maintenance of that implementation directly.
As these custom mocking implementations grow, logic inside of mock servers becomes complex and counter-productive. Writing automated tests for both Frontends and Backends becomes more difficult and coupled in undesired ways.
@wayfair/gqmock
offers an easy way to seed the data returned by GraphQL
operations. It masks the complexities of managing a mock server implementation
and instead exposes a declarative API for expressing the deterministic data you
need in your tests. There is no additional overhead for adding more tests to
your test suite, and because each test has a unique context, running tests in
parallel is 💯 supported!
@wayfair/gqmock
is an HTTP server which means that you can use it also outside
of test environment for fast feature development. On top of that, if you cannot
use the Node.js API that ships with this module, you can easily have the mock
server running in a container and call the endpoints documented below to
interact with the server.
To get a local copy up and running follow these simple steps.
This module is distributed via npm which is bundled with node and
should be installed as one of your project's devDependencies
:
npm install --save-dev @wayfair/gqmock
or
for installation via yarn
yarn add --dev @wayfair/gqmock
NOTE: If you plan on using the fakerConfig
functionality of this library, you
must also install the @faker-js/faker
dev dependency.
GraphqlMockingService
Parameter Name | Required | Description | Type | Default |
---|---|---|---|---|
port |
No | Port used to run the mock server | number | 5000 |
subgraph |
No | Enable subgraph schema support | boolean | false |
async GraphqlMockingService.start
Starts the mocking server.
async GraphqlMockingService.stop
Stops the mocking server.
async GraphqlMockingService.registerSchema
Registers a schema with the mock server.
Parameter Name | Required | Description | Type | Default |
---|---|---|---|---|
schema |
Yes | A valid GraphQL schema | string | |
options |
No | Schema registration options | object | |
options.fakerConfig |
No | Map of fields to return realistic data using faker.js | object | {} |
options.subgraph |
No | Is the schema a subgraph schema | boolean | false |
GraphqlMockingService.createContext
Creates a new GraphqlMockingContext
instance with a unique sequenceId
Parameter Name | Required | Description | Type | Default |
---|---|---|---|---|
sequenceId |
No | A string to be used as a sequenceId | string | uuid |
GraphqlMockingContext
GraphqlMockingContext.sequenceId
A unique string used to match GraphQL requests with registered seeds.
sequenceId
can be attached to requests using a custom Apollo Link:
import {ApolloLink} from '@apollo/client';
const mockServiceLink = new ApolloLink((operation, forward) => {
operation.setContext(({headers = {}}) => ({
headers: {
...headers,
...(sequenceId ? {'mocking-sequence-id': sequenceId} : {}),
},
}));
return forward(operation);
});
async GraphqlMockingContext.operation
Registers a seed for a GraphQL operation.
Parameter Name | Required | Description | Type | Default |
---|---|---|---|---|
operationName |
Yes | Name of the GraphQL operation | string | |
seedResponse |
Yes | See specific properties | object | |
seedResponse.data |
No | Data to be merged with the default apollo server mock | object | {} |
seedResponse.errors |
No | Errors to return | object[] | |
operationMatchArguments |
No | Params used for matching a seed with GraphQL operations. By default matching is exact. | object | {} |
options |
No | See specific properties | object | {} |
options.usesLeft |
No | Uses left before discarding the seed | number | seed doesn't get discarded |
options.partialArgs |
No | Allow partial matching of query arguments with the seed arguments | boolean | false |
options.statusCode |
No | HTTP response status code of the response | number | 200 |
async GraphqlMockingContext.networkError
Registers a seed for a network error.
Parameter Name | Required | Description | Type | Default |
---|---|---|---|---|
operationName |
Yes | Name of the GraphQL operation | string | |
seedResponse |
Yes | Error that will be sent from /graphql endpoint | object or string or null | |
operationMatchArguments |
No | Params used for matching a seed with GraphQL operations. By default matching is exact. | object | {} |
options |
No | See specific properties | object | {} |
options.usesLeft |
No | Uses left before discarding the seed | number | seed doesn't get discarded |
options.partialArgs |
No | Allow partial matching of query arguments with the seed arguments | boolean | false |
options.statusCode |
No | HTTP response status code of the response | number | 500 |
http:localhost:<port>/graphql/:operationName?
This endpoint supports serving a GraphQL IDE from the mock servers /graphql
route. Three options are available:
GraphQLIDE.ApolloSandbox
: Serve's the
Apollo Sandbox
experience.GraphQLIDE.GraphiQL
: Serve's the latest version of
GraphiQLGraphQLIDE.None
: Disables the GraphQL IDE experience, and therefore this
endpointhttp:localhost:<port>/graphql/:operationName?
Send GraphQL queries to this endpoint to retrieve mocked data. Seeds are
overlaid onto the response if they were previously registered. The
mocking-sequence-id
needs to be sent with every request. This can be done
automatically by configuring a custom Apollo Link for an Apollo Client. In order
to use the registered seeds, the mocking-sequence-id
header needs to match the
sequeneceId
used when the seed was registered.
Parameter Name | Required | Description | Type | Default |
---|---|---|---|---|
body.operationName * |
Yes | Name of the GraphQL operation | string | |
body.query |
Yes | GraphQL query | string | |
body.variables |
No | GraphQL query variables | object | {} |
headers.mocking-sequence-id |
No | Unique id of the use case context used to connect or separate seeds | string |
*: body.operationName
is not required if the operationName
is provided in
the path.
http:localhost:<port>/schema/register
Schema needs to be registered first before mocked data can be retrieved.
Parameter Name | Required | Description | Type | Default |
---|---|---|---|---|
body.schema |
Yes | GraphQL SDL schema | string | |
body.options |
No | See specific options | object | {} |
body.options.fakerConfig |
No | Faker.js config for GraphQL type fields | object | {} |
body.options.subgraph |
No | Is the schema a subgraph schema | boolean | false |
headers.mocking-sequence-id |
Yes | Unique id of the use case context used to connect or separate seeds | string |
http:localhost:<port>/seed/operation
Use this endpoint to register operation seeds. You can register multiple seeds for the same operation. If there are multiple matched seeds then the one registered first will be used.
Parameter Name | Required | Description | Type | Default |
---|---|---|---|---|
body.sequenceId |
Yes | Unique id of the use case context used to connect or separate seeds | string | |
body.operationName |
Yes | Name of the GraphQL operation | string | |
body.seedResponse |
Yes | See specific properties | object | |
body.seedResponse.data |
No | Data to be merged with the default apollo server mock | object | {} |
body.seedResponse.errors |
No | Errors to return | object[] | |
body.operationMatchArguments |
No | Params used for matching a seed with GraphQL operations. By default matching is exact. | object | {} |
body.options.usesLeft |
No | Uses left before discarding the seed | number | seed doesn't get discarded |
body.options.partialArgs |
No | Allow partial matching of query arguments with the seed arguments | boolean | false |
body.options.statusCode |
No | HTTP response status code of the response | number | 200 |
http:localhost:<port>/seed/network-error
Use this endpoint to register network errors caused by executing GraphQL queries. For example, you can simulate unauthorized access.
Parameter Name | Required | Description | Type | Default |
---|---|---|---|---|
body.sequenceId |
Yes | Unique id of the use case context used to connect or separate seeds | string | |
body.operationName |
Yes | Name of the GraphQL operation | string | |
body.seedResponse |
Yes | Error that will be sent from /graphql endpoint | object or string or null | |
body.operationMatchArguments |
No | Params used for matching a seed with GraphQL operations. By default matching is exact. | object | {} |
body.options |
No | See specific properties | object | {} |
body.options.usesLeft |
No | Uses left before discarding the seed | number | seed doesn't get discarded |
body.options.partialArgs |
No | Allow partial matching of query arguments with the seed arguments | boolean | false |
body.options.statusCode |
No | HTTP response status code of the response | number | 500 |
describe('App', function () {
let mockingService;
beforeAll(async () => {
mockingService = new GraphqlMockingService({port: 5000});
await mockingService.start();
const schema = fs.readFileSync(
path.resolve(__dirname, './schema.graphql'),
'utf-8'
);
await mockingService.registerSchema(schema);
});
afterAll(async () => {
await mockingService.stop();
});
it('should work', async () => {
const seed = {
data: {
booksByGenreCursorConnection: {
edges: [
{},
{},
{
node: {
id: 'Qm9vazo1',
title: 'Harry Potter and the Chamber of Secrets',
author: {
id: 'QXV0aG9yOjE=',
fullName: 'J. K. Rowling',
__typename: 'Author',
},
__typename: 'Book',
},
__typename: 'BooksEdge',
},
],
},
},
};
const mockingContext = mockingService.createContext();
await mockingContext.operation('GetBooks', seed, {genre: 'ALL'});
render(<App sequenceId={mockingContext.sequenceId} />);
const books = await screen.findAllByText(
'Harry Potter and the Chamber of Secrets'
);
expect(books.length).toEqual(3);
});
});
const context = service.createContext();
await context.operation(/* ... */).operation(/* ... */).networkError(/* ... */);
data
is one of the allowed properties for seedResponse
registration
parameter. It is supposed to mimic the query response defined by the registered
schema. As such, data
will be a composition of objects and arrays all the way
to primitive leaf fields. You can define objects in the following way:
const seedResponse = {
data: {
productBySku: {
name: 'Flagship Table with Sku',
},
},
};
const seedResponse = {
data: {
productBySku: {
name: 'Flagship Table with Sku',
variants: [
{},
{
name: 'standing',
tags: [{}, {value: 'adjustable'}],
},
{},
{
name: 'office',
tags: [{}, {value: 'adjustable'}],
},
],
},
},
};
In this example, variants
is a list of 4 elements, and tags
is a list of 2
elements. Only variants 2 and 4 will have seeded values.
The same lists can be defined using $length
to define how many elements a list
should have. $<index>
is used to override selected items in the list. The
prefix for both operations can be defined when the mocking service is
initialized.
const seedResponse = {
data: {
productBySku: {
name: 'Flagship Table with Sku',
variants: {
name: 'office',
tags: {value: 'adjustable', $length: 2},
$length: 4,
$2: {name: 'standing', tags: {value: 'adjustable', $length: 2}},
},
},
},
};
const schema = `
type ProductVariant {
name: String
color: String
tags: [Tag]
pictures: [Picture]
}
type Dimensions {
length: Int
width: Int
height: Int
}
type Product {
name: String
variants: [ProductVariant]
dimensions: Dimensions
}`;
const fakerConfig = {
Product: {
name: {
method: 'random.alpha',
args: {count: 5, casing: 'upper', bannedChars: ['A']},
},
},
Dimensions: {
length: {
method: 'random.numeric',
args: 2,
},
width: {
method: 'random.numeric',
args: [2],
},
height: {
method: 'random.numeric',
args: 3,
},
},
ProductVariant: {
name: {
method: 'random.words',
},
},
};
const mockingService = new GraphqlMockingService();
await mockingService.start();
await mockingService.registerSchema(schema, {fakerConfig});
query getProduct {
product {
name
}
}
will resolve as:
{
"data": {
"product": {
"name": "DTCIC"
}
}
}
sequenceId
as a header if it is present.ApolloClient
's link chainimport {ApolloLink, HttpLink, concat} from '@apollo/client';
import fetch from 'cross-fetch';
const setCustomHeaders = new ApolloLink((operation, forward) => {
operation.setContext(({headers = {}}) => ({
headers: {
...headers,
...(sequenceId ? {'mocking-sequence-id': sequenceId} : {}),
},
}));
return forward(operation);
});
const httpLink = new HttpLink();
const client = new ApolloClient({
// other configuration here
link: concat(setCustomHeaders, httpLink),
});
Start the mocking server and register a schema
import GraphqlMockingService from '@wayfair/gqmock';
const mockingService = new GraphqlMockingService({port: 5000});
await mockingService.start();
await mockingService.registerSchema(schema, options); // or register schema by calling the endpoint documented above
That's it. You can now register seeds and call /graphql
endpoint to get seeded
data.
GQMock was written with Node.js in mind. However, the mocking server can be dockerized and used in any environment. A docker image can be built at any time by running
docker build . -t wayfair-incubator/gqmock
Then you can run the container
docker run -dp <port on local port>:5000 wayfair-incubator/gqmock
The running server accepts requests to all documented endpoints on the port specified when starting the container.
See the open issues for a list of proposed features (and known issues).
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. For detailed contributing guidelines, please see CONTRIBUTING.md
Distributed under the MIT
License. See LICENSE
for more
information.
Project Link: https://github.com/wayfair-incubator/gqmock
This template was adapted from https://github.com/othneildrew/Best-README-Template.