0.5.0 • Published 5 years ago

graphql-mock-factory v0.5.0

Weekly downloads
5,292
License
Apache-2.0
Repository
github
Last release
5 years ago

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.

import { mockServer } from 'graphql-mock-factory';

// 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 = mockServer(schemaDefinition);

const query = `
  query {
    viewer {
      firstName
      lastName
    }
  }
`;

mockedServer(query);
// ->
// { data: { viewer: { 
//    firstName: 'lorem ipsum dolor sit amet', 
//    lastName: 'sed do eiusmod tempor incididunt' } } }
// 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.
  automockScalars(scalarMocks),

  // 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`.
mockServer(schemaDefinition, mocks, null)

// Or you can override the default mocks to only automock 
// certain fields. Here only enums and lists are automocked:
mockServer(schemaDefinition, mocks, [automockEnums, automockLists])

// 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.
    firstName: () => faker.name.firstName(),
    lastName: () => faker.name.lastName(),
  },
};

const mockedServer = mockServer(schemaDefinition, mocks);

...

mockedServer(query, mocks);
// ->
// { data: { 
//    viewer: { firstName: 'Jason', lastName: 'Wilde' } } }
// 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 = mockServer(schemaDefinition, {}, null);

...

mockedServer(query);
// 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`).

mockedServer(query, mocks, 
  // Pass a `mockOverride` object as the 3rd parameter
  { 
    viewer: { 
      firstName: 'Oscar',
    }
  },
);
// ->
// { data: { 
//    viewer: { firstName: 'Oscar', lastName: 'Smith' } } }
// Here we only specify `viewer.aliasedName`. 
// `viewer.firstName` will be generated by its corresponding 
// mock function (ie `User.firstName`).

mockedServer(`
  query {
    viewer { 
      firstName 
      aliasedName: firstName
    }
  }`,
  {}, 
  { 
    viewer: { 
      firstName: 'Oscar' 
    }
  },
);
// ->
// { data: { viewer: 
//   { firstName: 'Oscar', aliasedName: 'Lee' } } }
// 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
  }
`;

...

mockedServer(`
  query {
    viewer { 
      firstName
      parent {
        firstName
      }
    }
  }`,
  {}, 
  { 
    viewer: { 
      firstName: 'Oscar' 
    }
  },
);
// ->
// { data: { viewer: 
//   { firstName: 'Oscar', 
//     parent: { firstName: 'Krystina' } } } }
// `undefined` is equivalent to not specifying a value.
// `null` always nullifies the field.

const schemaDefinition = `
  ...

  type User {
    firstName: String
    parent: User
  }
`;

...

mockedServer(`
  query {
    viewer { 
      firstName
      parent {
        firstName
      }
    }
  }`,
  {}, 
  { 
    viewer: {
      firstName: undefined,
      parent: null 
    }
  },
);
// ->
// { data: { viewer: 
//   { firstName: 'Raegan', parent: null } } }

Accessing field arguments

Field arguments can be accessed the same way in mock functions and in mockOverride objects.

// Field arguments are passed as named parameters to mock functions.

const schemaDefinition = `
  type Query {
    echo(input: String): String
  }
`;

const mocks = {
  Query: {
    echo: ({input}) => `echo: ${input}`,
  }
};

...

mockedServer(`
  query {
    echo(input: "hello")
  }`
);
// ->
// { data: { 
//    echo: 'echo: hello' } }
// When `mockOverride` object contains functions, field arguments
// are passed in as named parameters to mock functions.

const schemaDefinition = `
  type Query {
    echo(input: String): String
  }
`;

mockedServer(`
  query {
    echo(input: "hello")
  }`,
  {},
  {
    echo: ({input}) => `repeat: ${input}`,
  }
);
// ->
// { 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.

// `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: {
    searchUser: ({name}) => ({
      firstName: `${name}`
    }),
  },
  User: {
    firstName: () => faker.name.firstName(),
    lastName: () => faker.name.lastName(),
  },
};

...

mockedServer(`
  query {
    searchUser(name: "Oscar") {
      firstName
      lastName
    }
  }
`)
// ->
// { data: 
//   { searchUser: 
//      { firstName: "Oscar", lastName: "Simpsons" } } }
// `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: {
    searchUser: ({country}) => ({
      address: {
        country: `${country}`,
      },
    }),
  },
  User: {
    firstName: () => faker.name.firstName(),
  },
  Address: {
    country: () => faker.address.country(),
  },
};

...

mockedServer(`
  query {
    searchUser(name: "France") {
      firstName
      address {
        country
      }
    }
  }
`)
// ->
// { data: 
//   { searchUser: 
//      { firstName: "Sam",  
//        address: { country: "France" } } }

Mocking lists

Lists must be mocked with the mockList function.

const schemaDefinition = `
  ...

  type User {
    name: String
    friends: [User]
  }
`;

const mocks = {
  User: {
    // Generates a list with 2 items.
    friends: mockList(2),
    name: () => faker.name.firstName(),
  }
};

mockedServer(`
  viewer {
    friends {
      name
    }
  }`
);
// ->
// { data: 
//   { viewer: 
//      { friends: [ { name: 'Nikki' }, { name: 'Doug' } ] } } }
// 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: {
    name: () => faker.name.firstName(),
    friends: mockList(2, ({pageNumber}, index) => ({
      name: `Friend #${index} - Page #${pageNumber}`,
    })),
  }
};

...

mockedServer(`
  viewer {
    friends(pageNumber: 0) {
      name
    }
  }`
);
// ->
// { 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.

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

import { mockList, mockServer } from 'graphql-mock-factory';

...

const mocks = {
  User: {
    name: () => faker.name.firstName(),
    friends: mockList(2, ({}, index) => ({name: `Friend #${index}`})),
  }
};

...

// Here we specify that the list will be of size 3
mockedServer(query, {}, {
  viewer: {
    friends: [
      // An empty object means the list item will be fully generated by 
      // its corresponding mock functions.
      {},
      // A partial object will be deep merged with the result of the 
      // corresponding mock functions.
      {'name': 'Oscar'}, 
      // `null` means the list item will be null. 
      null
    ],
  },
});
// ->
// { data:
//   { viewer:
//      { friends: [ { name: 'Friend #0' }, { name: 'Oscar' }, null ] } } }
// 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: {
    name: () => faker.name.firstName(),
    friends: mockList(2, ({}, index) => ({name: `Friend #${index}`})),
  }
};

const query = `
  query {
    viewer {
      friends {
        name
        aliasedName1: name
        aliasedName2: name
      }
    }
  }
`

mockedServer(query, {}, {
  viewer: {
    // The list will be of size 1.
    friends: mockList(1, () => ({
      // This will be deep merged with the result of the other `mockList` function.
      aliasedName1: 'Aliased Name 1',
    })),
  },
});
// ->
// { 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.

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!
  }
`;

...

mockedServer(`
  query {
    node(id: 'USER_ID') {
      ... on User {
        name
      }
    }
  }`, {}, {
    node: {
      __typename: 'User',
      name: 'Oscar',
    }
  }
);
// ->
// { data: 
//   { node: 
//      { name: 'Oscar } }
// TODO Add example

Simulating field errors

A field error can be simulated by including an Error instance in mockOverride.

mockedServer(`
  query {
    viewer {
      firstName
      lastName
    }
  }`, {}, {
  viewer: {
    // Errors shall not be thrown.
    firstName: Error('Could not fetch firstName.'),
  },
});
// ->
// { 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, `mockConnection` returns the number of requested nodes.
// Note that `hasNextPage` and `hasPreviousPage` behave as expected.

import { mockServer, mockConnection, automockRelay } from 'graphql-mock-factory';

...

const schemaDefinition = `
  type User implements Node {
    id: ID!
    name: String
    friends(before: String, after: String, first: Int, last: Int): UserConnection
  }

  ...
`;

const mocks = {
  User: {
    friends: mockConnection(),
    name: () => faker.name.firstName(),
  }
};

const mockedServer = mockServer(schemaDefinition, mocks);

mockedServer(`
  query {
    viewer {
      friends(first: 2) {
        edges {
          node {
            name
          }
          cursor
        }
        pageInfo {
          hasNextPage
          hasPreviousPage
        }
      }
    }
  }`,
);
// ->
// { data:
//  { viewer:
//     { friends:
//        { edges:
//           [ { node: { name: 'Milford' }, cursor: 'cursor_0' },
//             { node: { name: 'Bennie' }, cursor: 'cursor_1' } ],
//          pageInfo: { hasNextPage: true, hasPreviousPage: false } } } } }
// 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: mockConnection({maxSize: 1}),
    name: () => faker.name.firstName(),
  }
};

...

mockedServer(`
  query {
    viewer {
      friends(first: 2) {
        edges {
          node {
            name
          }
        }
        pageInfo {
          hasNextPage
          hasPreviousPage
        }
      }
    }
  }`,
);
// ->
// { data:
//  { viewer:
//     { friends:
//        { edges:
//           [ { node: { name: 'Milford' } } ],
//          pageInfo: { hasNextPage: false, hasPreviousPage: false } } } } }
// 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: mockConnection({nodeMock: ({ first, last }, index) => ({
      name: `Friend ${index} / ${first || last}`,
    })}),
    name: () => faker.name.firstName(),
  }
};

...

mockedServer(`
  query {
    viewer {
      friends(first: 2) {
        edges {
          node {
            name
          }
        }
        pageInfo {
          hasNextPage
          hasPreviousPage
        }
      }
    }
  }`,
);
// ->
// { data:
//  { viewer:
//     { friends:
//        { edges:
//           [ { node: { name: 'Friend 0 / 2' } },
//             { node: { name: 'Friend 1 / 2' } } ] } } } }
// An error is returned if the arguments do not conform the the Relay specs.

mockedServer(`
  query {
    viewer {
      friends(first: -2) {
        edges {
          node {
            name
          }
        }
      }
    }
  }`,
);
// ->
// { 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.

// Like `mockList`, it can be overriden with an array.
// This is because `mockConnection` is simply a wrapper around `mockList`.

const mocks = {
  User: {
    friends: mockConnection(),
    name: () => faker.name.firstName(),
  }
};

...

mockedServer(`
  query {
    viewer {
      friends(first: 5) {
        edges {
          node {
            name
          }
          cursor
        }
        pageInfo {
          hasNextPage
          hasPreviousPage
        }
      }
    }
  }`, {} , {
    viewer: {
      friends: {
        // Here only 3 items will be returned even though 5 were requested.
        edges: [
          {}, 
          {node: {'name': 'Oscar'}}, 
          null,
        ],
        pageInfo: {
          hasNextPage: false,
        },
      }
    }
  }
);
// ->
// { data:
//  { viewer:
//     { friends:
//        { edges:
//           [ { node: { name: 'Craig' }, cursor: 'cursor_0' },
//             { node: { name: 'Oscar' }, cursor: 'cursor_1' },
//             null ],
//          pageInfo: { hasNextPage: false, hasPreviousPage: false } } } } }
// TODO Add documentation
// TODO Add documentation

API Reference

mockServer()

mockServer(
  /**
   * The schema definition string.
   * It can be automatically generated by any GraphQL server.
   * See https://graphql.org/learn/schema/#type-language
   */
  schemaDefinition: string, 

  /**
   * Optional but recommended:
   * An object mapping fields to mock functions.
   * mocks[objectTypeName][fieldName] = mockFunction
   * 
   * All queried fields that are not automocked (via 'automocks`) 
   * must by manually mocked here.
   *
   * TODO Document interface mocks
   */
  mocks?: {[string]: {[string]: MockFunction}}, 

  /**
   * Optional: An array of function that returns a mock function for a field.
   *
   * This is a hook to define mock functions in a programmatic way.
   * The functions will be called for each field that has not been associated to 
   * a mock function in `mocks`. If the left-most function does not return 
   * anything for a field, then the following functions are called. 
   * If none of the functions returned anything for a field, then 
   * no mock function is attached to that field.
   * 
   * For example, this is can be used to automock custom scalars:
   *   mockServer(schemaDefinition, mocks, [
   *     ...defaultAutomocks,
   *     automockScalars({MyScalar: myScalarMock}),
   *   ])
   * 
   * The default value is `defaultAutomock`. It automatically
   * mocks:
   *  - `Boolean`, `ID`, `Int`, `Float` and `String` via `automockScalars(scalarMocks)`
   *  - lists via `getDefaultListMock`
   *  - enums via `automockLists`
   *  - Relay connections via `automockRelay`
   * You can pass `null` to disable all the default mocks.
   * See "Usage" > "Defining mock functions" > "Disable automocks"
   * 
   */
  automocks?: Array<
    (
      /**
      * The GraphQL type of the parent ObjectType containing the field.
      * @example: `User` from `User.name`
      */
      parentType : GraphQLType, 
      
      /**
      * The GraphQL field.
      * @example: `name` from `User.name`
      */
      field : GraphQLField,
    ) => MockFunction | void
  )
> = 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()

mockList(
  /**
   * The size of the mocked list.
   */
  size: number,

  /**
   * Optional: A mock function called for each list item.
   * 
   * @returns The return type has to match the type of the 
   *   field it is mocking.
   */
  itemMock?: (
    /**
     * The field arguments if any
     */
    fieldArguments: {[string]: any},

    /**
     * The index of the item in the mock list.
     */
    index: number,
  ) => any 
)

mockConnection()

mockConnection(
  /**
   * Optional: An object to configure the connection.
   */
  params?: {

    /**
     * Optional: The max size of the mocked collection. 
     */
    maxSize?: number, 

    /**
     * Optional: A mock function called for each node in 
     * the collection.
     */
    nodeMock?: ({[string]: any}, index) => any,
  },
)

FAQ

Why use this over graphql-tools mocking functionality?

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

Apache License 2.0

TODO

  • Add recipes for common client libraries
  • Fix Flow types
0.5.0

5 years ago

0.4.2

5 years ago

0.4.1

5 years ago

0.4.0

5 years ago

0.3.0

6 years ago

0.2.1

6 years ago

0.2.0

6 years ago

0.1.4

6 years ago

0.1.3

6 years ago

0.1.2

6 years ago

0.1.1

6 years ago

0.1.0

6 years ago

0.0.4

6 years ago

0.0.3

6 years ago

0.0.2

6 years ago

0.0.1

6 years ago