@4c/graphql-mocking
v0.0.4
Published
Quickly mock out any GraphQL API with examples and augmented with smart autogenerated data.
Downloads
14
Readme
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' }),
});