Skip to content

Commit

Permalink
feat: allow users to provider default mock resolvers (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
victor-guoyu authored Apr 12, 2021
1 parent 22b982b commit eaafe32
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 18 deletions.
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,56 @@ const mocks = {
</p></details>


#### Providing default mock resolver functions

If you have custom scalar types or would like to provide default mock resolver functions for certain types, you can pass your mock functions via the `resolvers` option.

<details>
<summary>See example</summary><p>

```js
const schema = gql`
type Shape = {
id: ID!
returnInt: Int
date: Date
nestedShape: Shape
}
type Query {
getShape: Shape
}
`;

const testQuery = gql`
{
getShape {
id
returnInt
date
nestedShape {
date
}
}
}
`;

const resp = ergonomock(schema, testQuery, {
resolvers: {
Date: () => "2021-04-09"
}
});
expect(resp.data).toMatchObject({
id: expect.toBeString(),
returnInt: expect.toBeNumber(),
date: '2021-04-09'
nestedShape {
date: '2021-04-09'
}
});
```
</p></details>

#### Mocking Errors

You can return or throw errors within the mock shape.
Expand Down Expand Up @@ -431,6 +481,7 @@ This component's props are very similar to Apollo-Client's [MockedProvider](http

- `mocks` is an object where keys are the operation names and the values are the `mocks` input that `ergonomock()` would accept. (i.e. could be empty, or any shape that matches the expected response.)
- `onCall` is a handler that gets called by any executed query. The call signature is `({operation: GraphQLOperation, response: any}) => void` where response is the full response being returned to that single query. The purpose of `onCall` is to provide some sort of spy (or `jest.fn()`) to make assertions on which calls went through, with which variables, and get a handle on the generated values from `ergonomock()`.
- `resolvers` is an object where the keys are the `gql` type and the values are the mock resolver functions. It's designed to allow users to override or provide a default mock function for certain types. (e.g. custom scalar types) The call signature is `(root, args, context, info) => any`.

<!-- ROADMAP -->
## Roadmap
Expand Down
161 changes: 161 additions & 0 deletions src/__tests__/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1126,4 +1126,165 @@ describe("Automocking", () => {
expect(resp.errors).toStrictEqual([new GraphQLError("foo shape")]);
});
});

describe("default mock resolvers", () => {
test("can mock partiallly resolved objects", () => {
const testQuery = /* GraphQL */ `
{
returnShape {
id
returnInt
returnIntList
returnString
returnFloat
returnBoolean
}
}
`;
const resp: any = ergonomock(schema, testQuery, {
resolvers: {
Shape: () => ({
// only returnInt and returnIntList are mocked,
// the rest of the results should be auto-mocked
returnInt: 1234,
returnIntList: [1234],
}),
},
});
expect(resp.data.returnShape).toMatchObject({
id: expect.toBeString(),
returnInt: 1234,
returnIntList: [1234],
returnString: expect.toBeString(),
returnFloat: expect.toBeNumber(),
returnBoolean: expect.toBeBoolean(),
});
});

test("it can mock partiallly resolved nested objects", () => {
const testQuery = /* GraphQL */ `
{
returnShape {
id
returnInt
returnString
nestedShape {
id
returnString
}
}
}
`;
const resp: any = ergonomock(schema, testQuery, {
resolvers: {
Shape: () => ({
// all returnString under Shape should return "Hello world!"
returnString: "Hello world!",
}),
},
});
expect(resp.data.returnShape).toMatchObject({
id: expect.toBeString(),
returnInt: expect.toBeNumber(),
returnString: "Hello world!",
nestedShape: {
id: expect.toBeString(),
returnString: "Hello world!",
},
});
});

test("can override basic scalar type", () => {
const testQuery = /* GraphQL */ `
{
returnShape {
returnInt
}
returnInt
returnListOfInt
}
`;
const resp: any = ergonomock(schema, testQuery, {
resolvers: {
Int: () => 1234,
},
});
expect(resp.data).toMatchObject({
returnShape: {
returnInt: 1234,
},
returnInt: 1234,
});
expect(resp.data.returnListOfInt.every((number) => number === 1234)).toBe(true);
});

test("can provide a default resolver for custom scalar type", () => {
const testQuery = /* GraphQL */ `
{
returnShape {
id
returnCustomScalar
}
}
`;
const resp: any = ergonomock(schema, testQuery, {
resolvers: {
CustomScalarType: () => "Custom scalar value",
},
});
expect(resp.data.returnShape.returnCustomScalar).toEqual("Custom scalar value");
});

test("can't override union type", () => {
const testQuery = /* GraphQL */ `
{
returnBirdsAndBees {
__typename
... on Bird {
id
}
... on Bee {
id
}
}
}
`;
const resp: any = ergonomock(schema, testQuery, {
resolvers: {
BirdsAndBees: () => ({
id: 1234,
}),
},
});
expect(resp.data.returnBirdsAndBees.find(({ id }) => id === 1234)).toBeUndefined();
});

test("mocks takes precedent over default mock resolvers", () => {
const testQuery = /* GraphQL */ `
query {
returnShape {
id
returnString
}
}
`;
const mocks = {
returnShape: {
returnString: "return string from mock",
},
};
const resp: any = ergonomock(schema, testQuery, {
mocks,
resolvers: {
Shape: () => ({
returnString: "return string from resolver",
}),
},
});
expect(resp.data.returnShape).toMatchObject({
id: expect.toBeString(),
returnString: "return string from mock",
});
});
});
});
2 changes: 2 additions & 0 deletions src/__tests__/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { buildSchemaFromTypeDefinitions } from "graphql-tools";

const schemaSDL = /* GraphQL */ `
scalar MissingMockType
scalar CustomScalarType
interface Flying {
id: ID!
returnInt: Int
Expand Down Expand Up @@ -41,6 +42,7 @@ const schemaSDL = /* GraphQL */ `
returnIDList: [ID]
nestedShape: Shape
nestedShapeList: [Shape]
returnCustomScalar: CustomScalarType
}
type RootQuery {
returnInt: Int
Expand Down
9 changes: 4 additions & 5 deletions src/apollo/ErgonoMockedProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React from "react";
import { GraphQLSchema, DocumentNode } from "graphql";
import {
ApolloClient,
DefaultOptions,
ApolloCache,
Resolvers,
ApolloLink,
InMemoryCache,
ApolloProvider,
NormalizedCacheObject
} from "@apollo/client";
import MockLink, { ApolloErgonoMockMap, MockLinkCallHandler } from "./MockLink";
import { GraphQLSchema, DocumentNode } from "graphql";
import { DefaultMockResolvers } from '../mock';

export interface ErgonoMockedProviderProps<TSerializedCache = {}> {
schema: GraphQLSchema | DocumentNode;
Expand All @@ -19,7 +19,7 @@ export interface ErgonoMockedProviderProps<TSerializedCache = {}> {
addTypename?: boolean;
defaultOptions?: DefaultOptions;
cache?: ApolloCache<TSerializedCache>;
resolvers?: Resolvers;
resolvers?: DefaultMockResolvers;
children?: React.ReactElement;
link?: ApolloLink;
}
Expand All @@ -40,8 +40,7 @@ export default function ErgonoMockedProvider(props: ErgonoMockedProviderProps) {
const c = new ApolloClient({
cache: cache || new InMemoryCache({ addTypename }),
defaultOptions,
link: link || new MockLink(schema, mocks || {}, { addTypename, onCall }),
resolvers
link: link || new MockLink(schema, mocks || {}, { addTypename, onCall, resolvers }),
});
setClient(c);
return () => client && ((client as unknown) as ApolloClient<any>).stop();
Expand Down
4 changes: 3 additions & 1 deletion src/apollo/MockLink.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ApolloLink, Operation, Observable, FetchResult } from "@apollo/client";
import { ErgonoMockShape, ergonomock } from "../mock";
import { ErgonoMockShape, ergonomock, DefaultMockResolvers } from "../mock";
import { GraphQLSchema, ExecutionResult, DocumentNode } from "graphql";
import stringify from "fast-json-stable-stringify";

type MockLinkOptions = {
addTypename: Boolean;
onCall?: MockLinkCallHandler;
resolvers?: DefaultMockResolvers;
};

export type ApolloErgonoMockMap = Record<
Expand Down Expand Up @@ -53,6 +54,7 @@ export default class MockLink extends ApolloLink {
mocks: mock || {},
seed,
variables: operation.variables,
resolvers: this.options.resolvers,
});

// Return Observer to be compatible with apollo
Expand Down
40 changes: 38 additions & 2 deletions src/apollo/__tests__/ErgonoMockedProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const QUERY_A = gql`
id
returnInt
returnString
returnCustomScalar
}
}
`;
Expand All @@ -29,8 +30,10 @@ const ChildA = ({ shapeId }: { shapeId: string }): ReactElement => {

return (
<div>
Component ChildA. returnString: {data.queryShape.returnString} returnInt:{" "}
{data.queryShape.returnInt}{" "}
Component ChildA.
<p>returnString: {data.queryShape.returnString}</p>
<p>returnInt: {data.queryShape.returnInt}</p>
<p>returnCustomScalar: {data.queryShape.returnCustomScalar}</p>
</div>
);
};
Expand Down Expand Up @@ -160,6 +163,39 @@ test("can mock the same operation multiple times with a function", async () => {
});
});

test("it allows the user to provide default mock resolvers", async () => {
const spy = jest.fn();
render(
<MockedProvider
schema={schema}
onCall={spy}
resolvers={{
Shape: (_, args) => ({
returnString: `John Doe ${args.id}`,
}),
}}
>
<Parent shapeId="123" />
</MockedProvider>
);

expect(await screen.findByText(/returnString: John Doe 123/)).toBeVisible();
expect(await screen.findByText(/returnInt: -?[0-9]+/)).toBeVisible();
const { operation, response } = spy.mock.calls[0][0];
expect(spy).toHaveBeenCalledTimes(1);
expect(operation.operationName).toEqual("OperationA");
expect(operation.variables).toEqual({ shapeId: "123" });
expect(response).toMatchObject({
data: {
queryShape: {
__typename: "Shape",
returnString: "John Doe 123",
returnInt: expect.toBeNumber(),
},
},
});
});

test("automocking is stable and deterministic per operation query, name and variable", async () => {
const spy1 = jest.fn();
const spy2 = jest.fn();
Expand Down
Loading

0 comments on commit eaafe32

Please sign in to comment.