0.0.4 • Published 3 years ago

@4c/graphql-mocking v0.0.4

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

GraphQL Mocking

Quickly mock out any GraphQL API with examples and augmented with smart autogenerated data.

Overview

At a high level graphql-mocking is just a graphql-js "server" that processes a request and resolves an return value. Instead of sourcing the data from an API or other backend, it uses an in memory graphql (like a simple ORM) built from example data and data generated from type information.

The core export of graphql-mocking is a MockStore instance which encapsulates a mocked GraphQL schema and the in memory data.

import Mocks from '@4c/graphql-mocking';
import graphql from from 'graphql';

const store = new Mocks(`
  type Root {
    person(id: ID): Person
  }

  type Person {
    name: String
  }

  schema {
    query: Root
  }
`)

const result = await graphql(store.mockedSchema, `
  query {
    person(id: 1) {
      name
    }
  }
`);

Without any additional configuration mockedSchema will resolve valid queries with seed generated data. Even better, a number of common schema patterns will be implemented automatically, such as Relay style Connections with pagination.

How data is generated.

Since testing UIs backed by a GraphQL server is a main use case. It's not sufficient to simply generate randon data. Data that changes every run makes for bad tests. To avoid this each field has access to a "seeded" data generator, which means data will be consistently generated for that field every time a resolution is run.

Customizing mocks

Generally fully generated data isn't sufficient for most mocking. Eventually you want to add specific examples and custom mocking. To accomplish this we need to introduce two concepts:

  • Mocks
  • Examples

Mocks

Mocks control schema resolution logic. They are similar in spirit to a GraphQL field resolver, expect they have a different "pivot". Normally a resolver is defined per field. A schema have many different types with the Person field type, and each one defines it's own resolver from the source object. Mocks, work per type instead, meaning you can define how *any Person is resolved regardless of it's parent type.

This is a powerful way to define schema behavior without needing to clarify behavior for every usage of a type in your schema. For instance we can implement a lookup for fields with arguments:

// fake data
const people = {
  1: { name: 'James' },
  2: { name: 'Besty' },
};

// Mock the 'person' field on the tooy Query type
store.mock('Query', () => ({
  person: (args, context, info) => people[args.id],
}));

const result = await graphql(
  store.mockedSchema,
  gql`
    query {
      person(id: 1) {
        name
      }
    }
  `,
);

result.data.person; // { name: 'James' }

Mocks return a "source" object used by GraphQL to resolve the value of the type, you are mocking. For an overview of what this entails we suggest reading: https://graphql.org/graphql-js/object-types/ but quickly, mocks can return a concrete value, as in the case of scalars like String, Boolean, etc. Or an object with functions that return a concrete value.

store.mock('String', () => 'hello world'); // 'hello world' will be used for all strings

store.mock('Person', () => ({
  name: () => generateName(),
}));

AS seen in the person example above. Mock field resolvers are also based the field arguments as well as the graphql context and info objects.

Examples

Examples are static data for a graphql type, think of it as the data from your database that provides the source objects for GraphQL resolution. For instance the following are "examples" of a Person from our schema:

const people = store.addExamples('Person', [
  {
    id: 'a575bf7b-3eda-4015-98f9-6077a68a91e8',
    name: 'James',
    age: 68,
  },
  {
    id: 'e78ac19b-6d06-401e-9ff4-6a4f20dea718',
    name: 'Betsy',
    age: 42,
  },
]);

When you add examples, they are used as a pool to pull from when resolving types. Examples don't need to be conprehensive, any fields in the GQL type that don't have a corresponding example field will be generated normally.

Examples often differ structurally from the GraphQL type they resolve to! For instance our Person might look like:

type Person {
  id: ID
  personId: String
  name: String!
  isRetirementAge: Boolean
}

Here instead of exposing age directly, Person defines isRetirementAge which is derived from age. However, when we try and add an example with age we get an error:

store.addExample('Person', {
  id: 'e78ac19b-6d06-401e-9ff4-6a4f20dea718',
  name: 'Betsy',
  age: 42,
});

// TypeError: `age does not exist on type Person`

This is helpful guardrail to ensure that our mock data is explicit about which properties map to GraphQL fields. If we want to explicitly deviate we need to prefix our field with $ to mark it as "internal".

store.addExample('Person', {
  id: 'e78ac19b-6d06-401e-9ff4-6a4f20dea718',
  name: 'Betsy',

  $age: 42,
});

Now we can pair our example with a Mock to derive the correct value for isRetirementAge.

const store = new Mocks(schema);

store.addExample('Person', {
  id: 'e78ac19b-6d06-401e-9ff4-6a4f20dea718',
  name: 'Betsy',
  age: 42,
});

store.mock('Person', () => ({
  isRetirementAge() {
    // this is the source object in a graphql resolver
    this.$age >= 65;
  },
}));

Defining a graph

Examples provide the mocked schema with concrete values to use when resolving types, as well as defining relationships between data in the graph. As with databases, examples should provide a primary key (by default either id or $id). Pks are used to create explicit relationships between other examples in the graph.

Consider the following schema defniing Person and blog Post:

type Post {
  id: ID
  title: String
  content: String
}

type Person {
  id: ID
  name: String!
  posts: [Post]
}

If we wanted to add examples that defined links we could do so like:

store.addExample('Person', [
  {
    id: 'person1',
    name: 'James',
  },
  {
    id: 'person2',
    name: 'Betsy',
  },
]);

store.addExamples('Post', [
  {
    id: 'post1',
    $personId: 'person1',
    title: 'Building a graphql mocking library',
  },
  {
    id: 'post2',
    $personId: 'person1',
    title: 'Funny looking birds',
  },
  {
    id: 'post3',
    $personId: 'person2',
    title: 'The Ultimate Answer',
  },
]);

Now we can relate these two types with a mock using the built-in related helper

import { related } from '@4c/graphql-mocking';

store.mock('Person', () => ({
  posts: related({
    relatedFieldName: '$personId',
    idField: '$id',
  }),
}));

now when we query for posts on people it will "Just Work"

const result = await graphql(
  store.mockedSchema,
  gql`
    query {
      person(id: "person1") {
        name
        posts {
          title
        }
      }
    }
  `,
);
// results in
data: {
  person: {
    name: 'James',
    posts: [
      { title: 'Building a graphql mocking library' },
      { title: 'Funny looking birds' }
    ]
  }
}

(This works for one-to-many or one-to-one relationships equally well).

Because this is such a common pattern, the library will automatically set up these relationships if it can infer from the example and type.

Heads: Internal keys that end with Id are automatically considered foreign key to it's connected type.

The mocking is also smart enough to infer fields as foreign keys if the schema type for the field is an object type and the example value is a string, it will assume it's an id reference.

store.addExamples('Post', [
  {
    $id: 'post1',
    person: 'person1',
    title: 'Building a graphql mocking library',
  },

No other configuration needed.

Connections and Relay

graphql-mocking, comes with out of the box support for Relay schema additions, which include:

Node Interface

When the schema has a Node interface and node query field, graphql-mocking will automatically mock them to work with example data.

query {
  node(id: "RmlsbToy") {
    ... on Person {
      name
    }
  }
}

Global IDs

In addition a specialized ID scalar mock is configured to return Relay compatible "global Ids", which are base64 encodings of the type name and local identifier. Note this requires that examples use a different field for their id than id, we recommend $id since it works out of the box.

store.addExample('Person', [
  {
    $id: 'person1',
    name: 'James',
  },
  {
    $id: 'person2',
    name: 'Betsy',
  },
]);

const result = await graphql(
  store.mockedSchema,
  gql`
    query {
      people {
        name
      }
    }
  `,
);

Connections

Pagination with connections also works out of the box just like List type generation and inference. Connections can also be configured directly via:

import { connection } from '@4c/graphql-mocking/relay';

store.mock('Person', {
  postConnection: connection({ relatedIdField: '$personId' }),
});