graphql-stateful-mock

Mock a GraphQL schema — but in a stateful way

Usage no npm install needed!

<script type="module">
  import graphqlStatefulMock from 'https://cdn.skypack.dev/graphql-stateful-mock';
</script>

README

graphql-stateful-mock

npm version

Mock a GraphQL schema — but in a stateful way.

Very similar to GraphQL tools mocking but produce a schema that is:

  • consistent: the same query with the same arguments will, by default, return the exact same result
  • mutable: developer can implement mutations that will modify the results for other queries

Install

npm install graphql-stateful-mock -D

Usage

import { makeExecutableSchema } from '@graphql-tools/schema';
import { addMocksToSchema } from 'graphql-stateful-mock';
import { graphql } from 'graphql';

// Fill this in with the schema string
const schemaString = `...`;

// Make a GraphQL schema with no resolvers
const schema = makeExecutableSchema({ typeDefs: schemaString });

// Create a new schema with mocks
const schemaWithMocks = addMocksToSchema({ schema });

const query = `
query tasksForUser {
  user(id: 6) { id, name }
}
`;

graphql(schemaWithMocks, query).then((result) => console.log('Got result', result));

// the same query will produce the exact same result 
graphql(schemaWithMocks, query).then((result) => console.log('Got result 2n time', result));

Internally, the state is stored in a MockStore that user can use to get and set mocks values. You can initiate the store independently to access it:


// Create a MockStore for this schema
const store = createMockStore({ schema });

// Create a new schema with the mock store
const schemaWithMocks = addMocksToSchema({ schema, store });

Recipes

Customizing mocks

Use the mocks option on addMocksToSchema to customize the generated mock values.

Define mocks with one function by field:

const mocks = {
  User: {
    name: () => casual.name(),
  }
}

Or by defining one function by type:

const mocks = {
  User: () => ({
    name: casual.name(),
    age: casual.integer(0, 120),
  }),
}

Custom scalar

const mocks = {
  DateTime: () => casual.date(),
}

Lists

To define a mock for a list, simply return an empty array of the desired length as mock value for the field:

const mocks = {
  User: {
    friends: () => [...new Array(casual.integer(2, 6))],
  }
}

Optionally, you can specify field values for the elements of the list:

const mocks = {
  User: {
    friends: () => [
      ...new Array(casual.integer(2, 6))
    ]
    // all my friends are between 21 and 30
    .map(() => ({ age: casual.integer(21, 30)})),
  }
}

Abstract types

If you'd like to provide a mock for an Union or Interface type, you need to provide the type with an extra __typename.

const typeDefs = `
  ...
  union Result = User | Book
`;
const mocks = {
  Result: () => ({
    __typename: 'User',
    name: casual.name(),
  })
}

Appplying mutations

Use resolvers option of addMocksToSchema to implement custom resolvers that interact with the store, especially to mutate field values in store.

const typeDefs = `
type User {
  id: Id!
  name: String!
}
type Query {
  me: User!
}
type Mutation {
  changeMyName(newName: String!): User!
}
`
const schema = makeExecutableSchema({ typeDefs: schemaString });
const schemaWithMocks = addMocksToSchema({
  schema,
  resolvers: (store) => ({
    Mutation: {
      changeMyName: (_, { newName }) => {
        // special singleton types `Query` and `Mutation` will use the key `ROOT`

        // this will set the field value for the `User` entity referenced in field
        // `me` of the singleton `Query`
        store.set('Query', 'ROOT', 'me', { name: newName });

        return store.get('Query', 'ROOT', 'me');
      }
    }
  })
});

As a result, any query that queries the field name of the User referenced in me will get the updated value.

Note the sugar signature of set:

store.set('Query', 'ROOT', 'me', { name: newName });

// is equivalent to:
const meRef = store.get('Query', 'ROOT', `me`) as Ref;
store.set(meRef, 'name', newName);

Handling *byId fields

By default, *byId (like userById(id: ID!)) field will return an entity that does not have the same id as the one queried. We can fix that:

const typeDefs = `
type User {
  id: Id!
  name: String!
}
type Query {
  userById(id: ID!): User!
}
`
const schema = makeExecutableSchema({ typeDefs: schemaString });
const schemaWithMocks = addMocksToSchema({
  schema,
  store,
  resolvers: (store) => ({
    Query {
      userById(_, { id }) => store.get('User', id),
    }
  })
});

Note that, by default, the id or _id field will be used as storage key and the store will make sure the storage key and the field value are equal. You can change the key field using the option typePolicies.

Implementing a pagination

The idea is that the store contains the full list, as field value, and that the resolver queries the store and slice the results:

const typeDefs = `
type User {
  id: Id!
  name: String!
  friends(offset: Int!, limit: Int!): [User!]!
}
type Query {
  me: User!
}
`
const schema = makeExecutableSchema({ typeDefs: schemaString });
const schemaWithMocks = addMocksToSchema({
  schema,
  store,
  resolvers: (store) => ({
    User: {
      // `addMocksToSchema` resolver will pass a `Ref` as `parent`
      // it contains a key to the `User` we are dealing with
      friends: (userRef, { offset, limit }) => {
        // this will generate and store a list of `Ref`s to some `User`s
        // next time we go thru this resolver (with same parent), the list
        // will be the same
        const fullList = store.get(userRef, 'friends') as Ref[];

        // actually apply pagination slicing
        return fullList.slice(offset, offset + limit)
      }
    }
  })
});

Relay-style pagination

The principles stay the same than for basic pagination:

const typeDefs = `
type User {
  id: Id!
  name: String!
  friends(offset: Int!, limit: Int!): FriendsConnection;
}
type FriendsConnection {
  totalCount: Int!
  edges: [FriendConnectionEdge!]!
}
type FriendsConnectionEdge {
  node: User!
}
type Query {
  me: User!
}
`
const schema = makeExecutableSchema({ typeDefs: schemaString });
const store = createMockStore({ schema });
const schemaWithMocks = addMocksToSchema({
  schema,
  store,
  resolvers: (store) => ({
    User: {
      friends: (userRef, { offset, limit }) => {

        const connectionRef = store.get(userRef, 'friends', 'edges');

        return {
          totalCount: edgesFullList.length,
          edges: edgesFullList.slice(offset, offset + limit)
      }
    }
  })
});

Related

  • graphql-tools#1682: [Feature request] Mocking: access generated mock objects for individual queries and mutations.
  • fejk-ql: a stateless GraphQL mock with stateful capabilities.