From eaafe328838ad93938a79f0c3b3a6ef02b07ddce Mon Sep 17 00:00:00 2001
From: Victor Guo
Date: Mon, 12 Apr 2021 11:27:48 -0400
Subject: [PATCH] feat: allow users to provider default mock resolvers (#113)
---
README.md | 51 ++++++
src/__tests__/lib.test.ts | 161 ++++++++++++++++++
src/__tests__/schema.ts | 2 +
src/apollo/ErgonoMockedProvider.tsx | 9 +-
src/apollo/MockLink.ts | 4 +-
.../__tests__/ErgonoMockedProvider.test.tsx | 40 ++++-
src/mock.ts | 36 ++--
7 files changed, 285 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index fee8fcb..f12ee0a 100644
--- a/README.md
+++ b/README.md
@@ -344,6 +344,56 @@ const mocks = {
+#### 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.
+
+
+See example
+
+```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'
+ }
+});
+```
+
+
#### Mocking Errors
You can return or throw errors within the mock shape.
@@ -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
diff --git a/src/__tests__/lib.test.ts b/src/__tests__/lib.test.ts
index ff035be..fb0d525 100644
--- a/src/__tests__/lib.test.ts
+++ b/src/__tests__/lib.test.ts
@@ -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",
+ });
+ });
+ });
});
diff --git a/src/__tests__/schema.ts b/src/__tests__/schema.ts
index 23a888f..fdabab5 100644
--- a/src/__tests__/schema.ts
+++ b/src/__tests__/schema.ts
@@ -2,6 +2,7 @@ import { buildSchemaFromTypeDefinitions } from "graphql-tools";
const schemaSDL = /* GraphQL */ `
scalar MissingMockType
+ scalar CustomScalarType
interface Flying {
id: ID!
returnInt: Int
@@ -41,6 +42,7 @@ const schemaSDL = /* GraphQL */ `
returnIDList: [ID]
nestedShape: Shape
nestedShapeList: [Shape]
+ returnCustomScalar: CustomScalarType
}
type RootQuery {
returnInt: Int
diff --git a/src/apollo/ErgonoMockedProvider.tsx b/src/apollo/ErgonoMockedProvider.tsx
index f5d0dd3..076d8fb 100644
--- a/src/apollo/ErgonoMockedProvider.tsx
+++ b/src/apollo/ErgonoMockedProvider.tsx
@@ -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 {
schema: GraphQLSchema | DocumentNode;
@@ -19,7 +19,7 @@ export interface ErgonoMockedProviderProps {
addTypename?: boolean;
defaultOptions?: DefaultOptions;
cache?: ApolloCache;
- resolvers?: Resolvers;
+ resolvers?: DefaultMockResolvers;
children?: React.ReactElement;
link?: ApolloLink;
}
@@ -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).stop();
diff --git a/src/apollo/MockLink.ts b/src/apollo/MockLink.ts
index ecb12dd..427aaf0 100644
--- a/src/apollo/MockLink.ts
+++ b/src/apollo/MockLink.ts
@@ -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<
@@ -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
diff --git a/src/apollo/__tests__/ErgonoMockedProvider.test.tsx b/src/apollo/__tests__/ErgonoMockedProvider.test.tsx
index 7468c49..b64c429 100644
--- a/src/apollo/__tests__/ErgonoMockedProvider.test.tsx
+++ b/src/apollo/__tests__/ErgonoMockedProvider.test.tsx
@@ -18,6 +18,7 @@ const QUERY_A = gql`
id
returnInt
returnString
+ returnCustomScalar
}
}
`;
@@ -29,8 +30,10 @@ const ChildA = ({ shapeId }: { shapeId: string }): ReactElement => {
return (
- Component ChildA. returnString: {data.queryShape.returnString} returnInt:{" "}
- {data.queryShape.returnInt}{" "}
+ Component ChildA.
+
returnString: {data.queryShape.returnString}
+
returnInt: {data.queryShape.returnInt}
+
returnCustomScalar: {data.queryShape.returnCustomScalar}
);
};
@@ -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(
+ ({
+ returnString: `John Doe ${args.id}`,
+ }),
+ }}
+ >
+
+
+ );
+
+ 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();
diff --git a/src/mock.ts b/src/mock.ts
index 51aca24..2b572b8 100644
--- a/src/mock.ts
+++ b/src/mock.ts
@@ -35,10 +35,15 @@ export type ErgonoMockShape = {
[k: string]: ErgonoMockShape | ErgonoMockLeaf | Array;
};
+export type DefaultMockResolvers = {
+ [k: string]: GraphQLFieldResolver
+};
+
export type ErgonomockOptions = {
mocks?: ErgonoMockShape;
seed?: string;
variables?: Record;
+ resolvers?: DefaultMockResolvers;
};
export function ergonomock(
@@ -73,6 +78,13 @@ export function ergonomock(
random.seed(seed);
+ const resolverOverrides: Map> = new Map();
+ if (options.resolvers) {
+ Object.entries(options.resolvers).forEach(([type, resolver]) =>
+ resolverOverrides.set(type, resolver)
+ );
+ }
+
const mockResolverFunction = function (
type: GraphQLType,
fieldName?: string
@@ -95,16 +107,6 @@ export function ergonomock(
return root[fieldName];
}
- // Default mock for enums
- if (fieldType instanceof GraphQLEnumType) {
- return getRandomElement(fieldType.getValues()).value;
- }
-
- // Automock object types
- if (isObjectType(fieldType)) {
- return { __typename: fieldType.name };
- }
-
// Lists
if (fieldType instanceof GraphQLList) {
return random
@@ -123,6 +125,20 @@ export function ergonomock(
);
}
+ if (resolverOverrides.has(fieldType.name)) {
+ return resolverOverrides.get(fieldType.name)!(root, args, context, info);
+ }
+
+ // Default mock for enums
+ if (fieldType instanceof GraphQLEnumType) {
+ return getRandomElement(fieldType.getValues()).value;
+ }
+
+ // Automock object types
+ if (isObjectType(fieldType)) {
+ return { __typename: fieldType.name };
+ }
+
// Mock default scalars
if (defaultMockMap.has(fieldType.name)) {
return defaultMockMap.get(fieldType.name)!(root, args, context, info);