Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/4catalyzer/graphql-mocking


https://github.com/4catalyzer/graphql-mocking

Last synced: about 2 months ago
JSON representation

Awesome Lists containing this project

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.

```js
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:

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

```js
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:

```js
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:

```graphql
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:

```ts
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".

```ts
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`.

```js
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`:

```graphql
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:

```ts
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

```js
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"

```js
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.

```js
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.

```graphql
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.

```js
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:

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

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