graphql-mock-factory
A JavaScript library to easily generate mock GraphQL responses. It lets you write robust UI tests with minimum fixture boilerplate. This is similar to graphql-tools
's mocking functionality but focused primarily on testing (see comparison).
Main features:
- Simple and intuitive API.
- Customize responses on a per query basis.
- Helpful validations and error messages.
- First class support for Relay connections.
- Optional mode to encourage writing mock functions.
Installation
graphql
is a peer-dependency:
npm install graphql-mock-factory graphql
Usage
Automocking your server
You can automock your entire schema in one line using your schema definition string.
; // Replace with your schema definition string.// It can be automatically generated by any GraphQL server.const schemaDefinition = ` type Query { viewer: User } type User { firstName: String lastName: String }`; // Initialize your mocked server.const mockedServer = ; const query = ` query { viewer { firstName lastName } }`; ;// ->// { data: { viewer: { // firstName: 'lorem ipsum dolor sit amet', // lastName: 'sed do eiusmod tempor incididunt' } } }
List of default mocks
// The `automocks` param (3rd param) of `mockServer` define what and // how fields are autmotically mocked. It takes an array of automock // functions. It defaults to:const defaultAutomocks = // Boolean are picked randomly. // ID are random uuid. // Int are random integers between 100 and -100. // Float are random float between 100 and -100. // String are random short "lorem ipsum" strings. // Enum values are picked randomly. automockEnums // All list are of size 2, ie mocked with `mockList(2)`. automockLists // All Relay connections return the number of requested // nodes, ie mocked with `mockConnection()`. automockRelay; // You can disable all default mocks by passing `null`. // Or you can override the default mocks to only automock // certain fields. Here only enums and lists are automocked: // You can also pass in your own automock functions.// This may be useful if your schema contains custom scalars.// See "API Reference" > "mockServer" > "automocks"
Defining mock functions
While fully automocking your schema is helpful to quickly get started, you can define your own mock functions for more realistic values. Your mock functions have full precedence over the default ones.
const mocks = User: // Here we use `faker-js` to generate realistic mock data. fakername fakername ; const mockedServer = ; ... ;// ->// { data: { // viewer: { firstName: 'Jason', lastName: 'Wilde' } } }
Tip to mock fields progressively
// In order to help you define realistic mock functions // progressively, you can disable some or all of the default mocks.// After that, an error will be thrown if a queried field is not // associated with a mock function. In other words, you won't have // to define a mock function for a field until it is queried for// the first time. ... // Setting `automocks` (ie 3rd parameter) to null will disable // all default mocks. To disable only certain default mocks, see// "Usage" > "Automocking your server" > "List of default mocks".const mockedServer = ; ... ;// Error ->// There is no base mock for 'Viewer.firstName'. // All queried fields must have a mock function.// ... OMITTED ...
Customizing responses per test
When writing a test, you can easily customize the mocked response by passing a mockOverride
object. The values that are not specified in the mockOverride
object will be generated by the mock functions.
// Here we specify `viewer.firstName`. // The other response field (ie `viewer.lastName`) will be generated // by its corresponding mock function (ie `User.lastName`). ;// ->// { data: { // viewer: { firstName: 'Oscar', lastName: 'Smith' } } }
Example with aliased fields
// Here we only specify `viewer.aliasedName`. // `viewer.firstName` will be generated by its corresponding // mock function (ie `User.firstName`). ;// ->// { data: { viewer: // { firstName: 'Oscar', aliasedName: 'Lee' } } }
Example with nested fields
// Here we only specify the `viewer.firstName`. // `viewer.parent.firstName` will be generated by its corresponding // mock function (ie `User.firstName`). const schemaDefinition = ` ... type User { firstName: String parent: User }`; ... ;// ->// { data: { viewer: // { firstName: 'Oscar', // parent: { firstName: 'Krystina' } } } }
Example with `null` and `undefined`
// `undefined` is equivalent to not specifying a value.// `null` always nullifies the field. const schemaDefinition = ` ... type User { firstName: String parent: User }`; ... ;// ->// { data: { viewer: // { firstName: 'Raegan', parent: null } } }
Accessing field arguments
Field arguments can be accessed the same way in mock functions and in mockOverride
objects.
Example in mock function
// Field arguments are passed as named parameters to mock functions. const schemaDefinition = ` type Query { echo(input: String): String }`; const mocks = Query: `echo: ` ; ... ;// ->// { data: { // echo: 'echo: hello' } }
Example in `mockOverride`
// When `mockOverride` object contains functions, field arguments// are passed in as named parameters to mock functions. const schemaDefinition = ` type Query { echo(input: String): String }`; ;// ->// { data: { // echo: 'repeat: hello' } }
Mocking nested fields
Objects returned by mock functions are deep merged with the return values of the mock functions of the nested fields.
Example with one level of nesting
// `searchUser.firstName` is generated by the mock function// of `Query.searchUser` because it returned an object with// a value for `firstName`. // `searchUser.lastName` is generated by the mock function // of `User.lastName` because the mock function of // `Query.searchUser` returned an object that did not include // a value for `lastName`. const schemaDefinition = ` type Query { searchUser(name: String): User } type User { firstName: String lastName: String }`; const mocks = Query: firstName: `` User: fakername fakername ; ... // ->// { data: // { searchUser: // { firstName: "Oscar", lastName: "Simpsons" } } }
Example with multiple levels of nesting
// `searchUser.address.country` is generated by the mock // function for `Query.searchUser` instead of the mock function // for `Address.country`. // `searchUser.firstName` is generated by the mock function // for `User.firstName` because the mock function for // `Query.searchUser` did not include a value for `firstName`. const schemaDefinition = ` type Query { searchUser(country: String): User } type User { firstName: String address: Address } type Address { country: String }`; const mocks = Query: address: country: `` User: fakername Address: fakeraddress ; ... // ->// { data: // { searchUser: // { firstName: "Sam", // address: { country: "France" } } }
Mocking lists
Lists must be mocked with the mockList
function.
The first parameter sets the size of the list
const schemaDefinition = ` ... type User { name: String friends: [User] }`; const mocks = User: // Generates a list with 2 items. friends: fakername ; ;// ->// { data: // { viewer: // { friends: [ { name: 'Nikki' }, { name: 'Doug' } ] } } }
List items can be customized with an optional function
// The function will be called for each list item with the field arguments // and the index of the item. // The return values of the function will be deep merged with the results // of the mock functions of the nested fields. const schemaDefinition = ` type User { name: String friends(pageNumber: Int): [User] } ...`; const mocks = User: fakername friends: ; ... ;// ->// { data: // { viewer: // { friends: [ { name: 'Friend #0 - Page #0' }, { name: 'Friend #1 - Page #0' } ] } } }
A mockList
can be overriden with an array or with another mockList
.
Overriding `mockList` with an array
// In most cases, a `mockList` is overriden with an array:// - The size of the array determines the length of the final array. // - The item objects will be deep merged with the return value of // the `mockList` function if provided. ; ... const mocks = User: fakername friends: ; ... // Here we specify that the list will be of size 3;// ->// { data:// { viewer:// { friends: [ { name: 'Friend #0' }, { name: 'Oscar' }, null ] } } }
Overriding `mockList` with another `mockList`
// In some cases it might be more convenient to override a // `mockList` with another `mockList`:// - The size of the `mockList` override determines the length // of the final array.// - The return values of the optional `mockList` functions // will be deep merged. const mocks = User: fakername friends: ; const query = ` query { viewer { friends { name aliasedName1: name aliasedName2: name } } }` ;// ->// { data:// { viewer:// { friends: [ { // name: 'Friend #0', aliasedName1: 'Aliased Name 1', aliasedName2: 'Katy' } ] } } }
Mocking interfaces and unions
In order to know which type to resolve to, mockOverride
must specify __typename
for fields
that return an interface or a union type.
Example for fields that return an interface type
const schemaDefinition = ` type Query { node(id: ID!): Node } type User implements Node { id: ID! name: String } type Address implements Node { id: ID! address: String } type Node { id: ID! }`; ... ;// ->// { data: // { node: // { name: 'Oscar } }
Example for fields that return a union type
// TODO Add example
Simulating field errors
A field error can be simulated by including an Error
instance in mockOverride
.
Example
;// ->// { errors: // [ { Error: Could not fetch firstName.// ... OMITTED ...// message: 'Could not fetch error',// locations: [ { line: 4, column: 7 } ],// path: [ 'viewer', 'firstName' ] } ],// data:// { viewer:// { firstName: null, lastName: 'Gold' } } }
Mocking Relay connections
mockConnection
is a convenience helper function to mock Relay connections.
By default, it returns the number of requested nodes
// By default, `mockConnection` returns the number of requested nodes.// Note that `hasNextPage` and `hasPreviousPage` behave as expected. ; ... const schemaDefinition = ` type User implements Node { id: ID! name: String friends(before: String, after: String, first: Int, last: Int): UserConnection } ...`; const mocks = User: friends: fakername ; const mockedServer = ; ;// ->// { data:// { viewer:// { friends:// { edges:// [ { node: { name: 'Milford' }, cursor: 'cursor_0' },// { node: { name: 'Bennie' }, cursor: 'cursor_1' } ],// pageInfo: { hasNextPage: true, hasPreviousPage: false } } } } }
The collection size can be set with `maxSize`
// The optional `maxSize` named parameter limits the number of returned items.// Note that `hasNextPage` and `hasPreviousPage` behave as expected. const mocks = User: // At most 1 item will be returned friends: fakername ; ... ;// ->// { data:// { viewer:// { friends:// { edges:// [ { node: { name: 'Milford' } } ],// pageInfo: { hasNextPage: false, hasPreviousPage: false } } } } }
Nodes can be customized with `nodeMock`
// The optional `nodeMock` named function customizes the requested nodes.// Like `mockList`, it is called with the connection field arguments and // the index of the node. const mocks = User: friends: fakername ; ... ;// ->// { data:// { viewer:// { friends:// { edges:// [ { node: { name: 'Friend 0 / 2' } },// { node: { name: 'Friend 1 / 2' } } ] } } } }
Field arguments are validated
// An error is returned if the arguments do not conform the the Relay specs. ;// ->// { errors:// [ { Error: First and last cannot be negative.// ... OMITTED ...// message: 'First and last cannot be negative.',// locations: [ { line: 4, column: 7 } ],// path: [ 'viewer', 'friends' ] } ],// data: { viewer: { friends: null } } }
mockConnection
can be overriden with an array, another mockConnection
or a mockList
.
Overriding with an array
// Like `mockList`, it can be overriden with an array.// This is because `mockConnection` is simply a wrapper around `mockList`. const mocks = User: friends: fakername ; ... ;// ->// { data:// { viewer:// { friends:// { edges:// [ { node: { name: 'Craig' }, cursor: 'cursor_0' },// { node: { name: 'Oscar' }, cursor: 'cursor_1' },// null ],// pageInfo: { hasNextPage: false, hasPreviousPage: false } } } } }
Overriding with a `mockList`
// TODO Add documentation
Overriding with another `mockConnection`
// TODO Add documentation
API Reference
mockServer()
Return a mocked server. This is the entry point of this lib.
> = defaultAutomocks; /** * Mock function * * @returns The return type has to match the type of the field it is mocking. * If it returns an object, the object will be deep merged with the * return values of the mock functions of the nested fields. * See "Usage" > "Mocking nested fields". */type MockFunction = /** * The field arguments */ params: string: any any; /** * Mock server * * @returns The GraphQL response */type MockServer = /** * The GraphQL query string. */ query: string /** * The GraphQL variables for the query. */ variables: string: any /** * An object that overrides the response generated by the * mock functions defined in `mocks`. * See "Usage" > "Overriding mocked functions with `mockOverride`". */ mockOverride: string: any Object
mockList()
Return a mock function for a list.
mockConnection()
Return a mock function for a Relay connection.
FAQ
graphql-tools
mocking functionality?
Why use this over graphql-tools
was the first to introduce the idea of mocking an entire GraphQL server using its schema definition. Unfortunately, we could not use it in our tests because of a few limitations:
- There is no way to customize the values of aliased fields (see example). This is a deal breaker when writing tests because you need to be able to customize any value that a test relies on.
- Resolver functions are ignored if the resolver function of their parent field returns an object (see example). This is an unexpected behavior that makes mocking non-scalar fields very confusing.
- It is not possible to customize mocks on a per query basis. There has been an attempt to work around this but it adds another layer of complexity (see example).
- There is no way to automock certain field types. That becomes quite repititive if you use Relay and need to mock all the connection fields (see example).
In addition to these limitations, its mocking API can be confusing. For example, it is not clear why object types must be mocked with resolver functions. Is it not enough to define resolver functions for fields? Similarly, it is not clear why root
, context
and info
parameters are passed to the mock functions. When does it make sense to access those parameters in a mock function?
graphql-mock-factory
aims to address all of these issues. It also has a few other features that make mocking easier:
- Special care has been taken to raise helpful validation and error messages.
- There is an option to enforce that realistic mock are defined and shared progressively (see doc).
- It comes with out-of-the-box mocks for Relay connections (see doc).
License
TODO
- Add recipes for common client libraries
- Fix Flow types