Repository: FormidableLabs/urql (ts)

Issue:
RFC: Graphcache warnings for configs
## Summary

Currently a couple of warnings in Graphcache are just dedicated to checking the user's input when "schema awareness" is active. In other words, when we have a schema, we check whether some fields even exist on the schema, and if they don't the user will see a warning: https://github.com/FormidableLabs/urql/blob/ecb58bfdd97ed660a2dd248fd51e44c07b7350b5/exchanges/graphcache/src/ast/schemaPredicates.ts#L62-L113

Such a warning doesn't exist for fields in the config, like `opts.updates.Mutation.oopsTypo`; It'd be great to add those and take this opportunity to also _test_ whether our existing warnings still work correctly!

## Proposed Solution

- Add checks for all `config` fields that relate to the `schema`, if one has been passed
- Add tests for `query` and `write` operations to check whether warnings don't mis-trigger

## Requirements

- When `opts.schema` has been passed, use new schema predicates to warn whether `opts.updates`, `opts.resolvers`, and `opts.optimistic` contain fields that don't exist on the schema
- When `opts.schema` has been passed, use new schema predicates to warn whether `opts.keys` contains typenames that don't exist on the schema 

Pull request description:
(graphcache) Validation for Graphcache's opts.updates, opts.resolvers, opts.optimistic
## Summary

As described in #810, when a schema is known, we want to validate `opts.keys`, `opts.updates`, `opts.resolvers`, and `opts.optimistic`.

When any of the validation fails, we want to warn the user via `console.warn`.

Validation for `opts.keys` has already been added in PR https://github.com/FormidableLabs/urql/pull/820

## Set of changes

* `opts.updates.Mutation` and `opts.updates.Subscription` are now validated against the schema.
* `opts.resolvers` is now validated against the schema.
* `opts.optimistic` is now validated against the schema.

Resolves #810.

Patch:
diff --git a/.changeset/thick-avocados-enjoy.md b/.changeset/thick-avocados-enjoy.md
index bc9db73b..6ac78570 100644
--- a/.changeset/thick-avocados-enjoy.md
+++ b/.changeset/thick-avocados-enjoy.md
@@ -2,5 +2,6 @@
 '@urql/exchange-graphcache': minor
 ---
 
-Issue warnings when an unknown type has been included in Graphcache's opts.key configuration to help spot typos.
-See: [#820](https://github.com/FormidableLabs/urql/pull/820)
+Issue warnings when an unknown type or field has been included in Graphcache's `opts` configuration to help spot typos.
+Checks `opts.keys`, `opts.updates`, `opts.resolvers` and `opts.optimistic`.
+See: [#820](https://github.com/FormidableLabs/urql/pull/820) and [#826](https://github.com/FormidableLabs/urql/pull/826)
diff --git a/docs/graphcache/errors.md b/docs/graphcache/errors.md
index 9906965d..bc9dd200 100644
--- a/docs/graphcache/errors.md
+++ b/docs/graphcache/errors.md
@@ -289,3 +289,46 @@ This error occurs when an unknown type is found in `opts.keys`.
 
 Check whether your schema is up-to-date, or whether you're using an invalid
 typename in `opts.keys`, maybe due to a typo.
+
+## (21) Invalid mutation
+
+> Invalid mutation field `???` is not in the defined schema but the `updates` option is referencing it.
+
+When you're passing an introspected schema to the cache exchange, it is
+able to check whether your `opts.updates.Mutation` is valid.
+This error occurs when an unknown mutation field is found in `opts.updates.Mutation`.
+
+Check whether your schema is up-to-date, or whether you've got a typo in `opts.updates.Mutation`.
+
+## (22) Invalid subscription
+
+> Invalid subscription field: `???` is not in the defined schema but the `updates` option is referencing it.
+
+When you're passing an introspected schema to the cache exchange, it is
+able to check whether your `opts.updates.Subscription` is valid.
+This error occurs when an unknown subscription field is found in `opts.updates.Subscription`.
+
+Check whether your schema is up-to-date, or whether you're using an invalid
+subscription name in `opts.updates.Subscription`, maybe due to a typo.
+
+## (23) Invalid resolver
+
+> Invalid resolver: `???` is not in the defined schema, but the `resolvers`
+> option is referencing it.
+
+When you're passing an introspected schema to the cache exchange, it is
+able to check whether your `opts.resolvers` is valid.
+This error occurs when an unknown query, type or field is found in `opts.resolvers`.
+
+Check whether your schema is up-to-date, or whether you've got a typo in `opts.resolvers`.
+
+## (24) Invalid optimistic mutation
+
+> Invalid optimistic mutation field: `???` is not a mutation field in the defined schema,
+> but the `optimistic` option is referencing it.
+
+When you're passing an introspected schema to the cache exchange, it is
+able to check whether your `opts.optimistic` is valid.
+This error occurs when a field in `opts.optimistic` is not in the schema's `Mutation` fields.
+
+Check whether your schema is up-to-date, or whether you've got a typo in `Mutation` or `opts.optimistic`.
diff --git a/exchanges/graphcache/src/ast/schemaPredicates.ts b/exchanges/graphcache/src/ast/schemaPredicates.ts
index 6fe83c23..3fedb774 100644
--- a/exchanges/graphcache/src/ast/schemaPredicates.ts
+++ b/exchanges/graphcache/src/ast/schemaPredicates.ts
@@ -10,7 +10,12 @@ import {
 } from 'graphql';
 
 import { warn, invariant } from '../helpers/help';
-import { KeyingConfig } from '../types';
+import {
+  KeyingConfig,
+  UpdatesConfig,
+  ResolverConfig,
+  OptimisticMutationConfig,
+} from '../types';
 
 export const isFieldNullable = (
   schema: GraphQLSchema,
@@ -131,3 +136,121 @@ export function expectValidKeyingConfig(
     });
   }
 }
+
+export function expectValidUpdatesConfig(
+  schema: GraphQLSchema,
+  updates: UpdatesConfig
+): void {
+  if (process.env.NODE_ENV === 'production') {
+    return;
+  }
+
+  /* eslint-disable prettier/prettier */
+  const schemaMutations = schema.getMutationType()
+    ? Object.keys((schema.getMutationType() as GraphQLObjectType).toConfig().fields)
+    : [];
+  const schemaSubscriptions = schema.getSubscriptionType()
+    ? Object.keys((schema.getSubscriptionType() as GraphQLObjectType).toConfig().fields)
+    : [];
+  const givenMutations = updates.Mutation
+    ? Object.keys(updates.Mutation)
+    : [];
+  const givenSubscriptions = updates.Subscription
+    ? Object.keys(updates.Subscription)
+    : [];
+  /* eslint-enable prettier/prettier */
+
+  for (const givenMutation of givenMutations) {
+    if (schemaMutations.indexOf(givenMutation) === -1) {
+      warn(
+        'Invalid mutation field: `' +
+          givenMutation +
+          '` is not in the defined schema, but the `updates.Mutation` option is referencing it.',
+        21
+      );
+    }
+  }
+
+  for (const givenSubscription of givenSubscriptions) {
+    if (schemaSubscriptions.indexOf(givenSubscription) === -1) {
+      warn(
+        'Invalid subscription field: `' +
+          givenSubscription +
+          '` is not in the defined schema, but the `updates.Subscription` option is referencing it.',
+        22
+      );
+    }
+  }
+}
+
+function warnAboutResolver(name: string): void {
+  warn(
+    `Invalid resolver: \`${name}\` is not in the defined schema, but the \`resolvers\` option is referencing it.`,
+    23
+  );
+}
+
+export function expectValidResolversConfig(
+  schema: GraphQLSchema,
+  resolvers: ResolverConfig
+): void {
+  if (process.env.NODE_ENV === 'production') {
+    return;
+  }
+
+  const validTypes = Object.keys(schema.getTypeMap());
+
+  for (const key in resolvers) {
+    if (key === 'Query') {
+      const queryType = schema.getQueryType();
+      if (queryType) {
+        const validQueries = Object.keys(queryType.toConfig().fields);
+        for (const resolverQuery in resolvers.Query) {
+          if (validQueries.indexOf(resolverQuery) === -1) {
+            warnAboutResolver('Query.' + resolverQuery);
+          }
+        }
+      } else {
+        warnAboutResolver('Query');
+      }
+    } else {
+      if (validTypes.indexOf(key) === -1) {
+        warnAboutResolver(key);
+      } else {
+        const validTypeProperties = Object.keys(
+          (schema.getType(key) as GraphQLObjectType).getFields()
+        );
+        const resolverProperties = Object.keys(resolvers[key]);
+        for (const resolverProperty of resolverProperties) {
+          if (validTypeProperties.indexOf(resolverProperty) === -1) {
+            warnAboutResolver(key + '.' + resolverProperty);
+          }
+        }
+      }
+    }
+  }
+}
+
+export function expectValidOptimisticMutationsConfig(
+  schema: GraphQLSchema,
+  optimisticMutations: OptimisticMutationConfig
+): void {
+  if (process.env.NODE_ENV === 'production') {
+    return;
+  }
+
+  const validMutations = schema.getMutationType()
+    ? Object.keys(
+        (schema.getMutationType() as GraphQLObjectType).toConfig().fields
+      )
+    : [];
+
+  for (const mutation in optimisticMutations) {
+    if (validMutations.indexOf(mutation) === -1) {
+      warn(
+        `Invalid optimistic mutation field: \`${mutation}\` is not a mutation field in the defined schema, but the \`optimistic\` option is referencing it.`,
+        24
+      );
+    }
+  }
+}
diff --git a/exchanges/graphcache/src/helpers/help.ts b/exchanges/graphcache/src/helpers/help.ts
index ff4c7634..2ebf7356 100644
--- a/exchanges/graphcache/src/helpers/help.ts
+++ b/exchanges/graphcache/src/helpers/help.ts
@@ -24,7 +24,11 @@ export type ErrorCode =
   | 17
   | 18
   | 19
-  | 20;
+  | 20
+  | 21
+  | 22
+  | 23
+  | 24;
 
 type DebugNode = ExecutableDefinitionNode | InlineFragmentNode;
 
diff --git a/exchanges/graphcache/src/store/store.ts b/exchanges/graphcache/src/store/store.ts
index c8fd2942..88ddfb8b 100644
--- a/exchanges/graphcache/src/store/store.ts
+++ b/exchanges/graphcache/src/store/store.ts
@@ -75,8 +75,31 @@ export class Store implements Cache {
       if (mutationType) mutationName = mutationType.name;
       if (subscriptionType) subscriptionName = subscriptionType.name;
 
-      if (this.keys) {
-        SchemaPredicates.expectValidKeyingConfig(this.schema, this.keys);
+      if (process.env.NODE_ENV !== 'production') {
+        if (this.keys) {
+          SchemaPredicates.expectValidKeyingConfig(this.schema, this.keys);
+        }
+
+        const hasUpdates =
+          Object.keys(this.updates.Mutation).length > 0 ||
+          Object.keys(this.updates.Subscription).length > 0;
+        if (hasUpdates) {
+          SchemaPredicates.expectValidUpdatesConfig(this.schema, this.updates);
+        }
+
+        if (this.resolvers) {
+          SchemaPredicates.expectValidResolversConfig(
+            this.schema,
+            this.resolvers
+          );
+        }
+
+        if (this.optimisticMutations) {
+          SchemaPredicates.expectValidOptimisticMutationsConfig(
+            this.schema,
+            this.optimisticMutations
+          );
+        }
       }
     }
 
diff --git a/scripts/jest/preset.js b/scripts/jest/preset.js
index 34e22cfe..080de0ef 100644
--- a/scripts/jest/preset.js
+++ b/scripts/jest/preset.js
@@ -3,6 +3,7 @@ module.exports = {
   setupFiles: [
     require.resolve('./setup.js')
   ],
+  clearMocks: true,
   transform: {
     '^.+\\.tsx?$': 'ts-jest',
   },
diff --git a/scripts/jest/setup.js b/scripts/jest/setup.js
index cd57a55c..78436ea7 100644
--- a/scripts/jest/setup.js
+++ b/scripts/jest/setup.js
@@ -1,3 +1,5 @@
+// This script is run before each `.test.ts` file.
+
 global.AbortController = undefined;
 global.fetch = jest.fn();
 
@@ -5,8 +7,6 @@ process.on('unhandledRejection', error => {
   throw error;
 });
 
-jest.restoreAllMocks();
-
 const originalConsole = console;
 global.console = {
   ...originalConsole,


Test patch:
diff --git a/exchanges/graphcache/src/operations/query.test.ts b/exchanges/graphcache/src/operations/query.test.ts
index 3f819546..fc260272 100644
--- a/exchanges/graphcache/src/operations/query.test.ts
+++ b/exchanges/graphcache/src/operations/query.test.ts
@@ -41,8 +41,6 @@ describe('Query', () => {
         ],
       }
     );
-
-    jest.resetAllMocks();
   });
 
   it('test partial results', () => {
@@ -155,6 +153,9 @@ describe('Query', () => {
       __typename: 'Query',
       todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }],
     });
+
+    expect(console.warn).not.toHaveBeenCalled();
+    expect(console.error).not.toHaveBeenCalled();
   });
 
   it('should respect altered root types', () => {
@@ -186,6 +187,9 @@ describe('Query', () => {
       __typename: 'query_root',
       todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }],
     });
+
+    expect(console.warn).not.toHaveBeenCalled();
+    expect(console.error).not.toHaveBeenCalled();
   });
 
   it('should allow subsequent read when first result was null', () => {
@@ -247,5 +251,8 @@ describe('Query', () => {
       __typename: 'query_root',
       todos: [null],
     });
+
+    expect(console.warn).not.toHaveBeenCalled();
+    expect(console.error).not.toHaveBeenCalled();
   });
 });
diff --git a/exchanges/graphcache/src/operations/write.test.ts b/exchanges/graphcache/src/operations/write.test.ts
index 16c2ceda..f4d16297 100644
--- a/exchanges/graphcache/src/operations/write.test.ts
+++ b/exchanges/graphcache/src/operations/write.test.ts
@@ -44,6 +44,33 @@ describe('Query', () => {
     jest.clearAllMocks();
   });
 
+  it('should not crash for valid writes', async () => {
+    const VALID_TODO_QUERY = gql`
+      mutation {
+        toggleTodo {
+          id
+          text
+          complete
+        }
+      }
+    `;
+    write(
+      store,
+      { query: VALID_TODO_QUERY },
+      {
+        __typename: 'Mutation',
+        toggleTodo: {
+          __typename: 'Todo',
+          id: '0',
+          text: 'Teach',
+          complete: true,
+        },
+      }
+    );
+    expect(console.warn).not.toHaveBeenCalled();
+    expect(console.error).not.toHaveBeenCalled();
+  });
+
   it('should warn once for invalid fields on an entity', () => {
     const INVALID_TODO_QUERY = gql`
       mutation {
@@ -82,7 +109,9 @@ describe('Query', () => {
       }
     );
     expect(console.warn).toHaveBeenCalledTimes(1);
-    expect((console.warn as any).mock.calls[0][0]).toMatch(/incomplete/);
+    expect((console.warn as any).mock.calls[0][0]).toMatch(
+      /The field `incomplete` does not exist on `Todo`/
+    );
   });
 
   it('should warn once for invalid fields on an entity', () => {
@@ -131,7 +160,9 @@ describe('Query', () => {
     );
 
     expect(console.warn).toHaveBeenCalledTimes(1);
-    expect((console.warn as any).mock.calls[0][0]).toMatch(/writer/);
+    expect((console.warn as any).mock.calls[0][0]).toMatch(
+      /The field `writer` does not exist on `Todo`/
+    );
   });
 
   it('should skip undefined values that are expected', () => {
@@ -146,7 +177,9 @@ describe('Query', () => {
     write(store, { query }, { field: undefined } as any);
     // Because of us writing an undefined field
     expect(console.warn).toHaveBeenCalledTimes(2);
-    expect((console.warn as any).mock.calls[0][0]).toMatch(/undefined/);
+    expect((console.warn as any).mock.calls[0][0]).toMatch(
+      /The field `field` does not exist on `Query`/
+    );
 
     InMemoryData.initDataState(store.data, null);
     // The field must still be `'test'`
diff --git a/exchanges/graphcache/src/store/store.test.ts b/exchanges/graphcache/src/store/store.test.ts
index 170ebbcb..8fdde197 100644
--- a/exchanges/graphcache/src/store/store.test.ts
+++ b/exchanges/graphcache/src/store/store.test.ts
@@ -5,6 +5,7 @@ import { query } from '../operations/query';
 import { write, writeOptimistic } from '../operations/write';
 import * as InMemoryData from './data';
 import { Store } from './store';
+import { noop } from '../test-utils/utils';
 
 const Appointment = gql`
   query appointment($id: String) {
@@ -91,6 +92,85 @@ describe('Store', () => {
   });
 });
 
+describe('Store with UpdatesConfig', () => {
+  it("sets the store's updates field to the given argument", () => {
+    const updatesOption = {
+      Mutation: {
+        toggleTodo: noop,
+      },
+      Subscription: {
+        newTodo: noop,
+      },
+    };
+
+    const store = new Store({
+      updates: updatesOption,
+    });
+
+    expect(store.updates.Mutation).toBe(updatesOption.Mutation);
+    expect(store.updates.Subscription).toBe(updatesOption.Subscription);
+  });
+
+  it("sets the store's updates field to an empty default if not provided", () => {
+    const store = new Store({});
+
+    expect(store.updates.Mutation).toEqual({});
+    expect(store.updates.Subscription).toEqual({});
+  });
+
+  it('should not warn if Mutation/Subscription operations do exist in the schema', function () {
+    new Store({
+      schema: require('../test-utils/simple_schema.json'),
+      updates: {
+        Mutation: {
+          toggleTodo: noop,
+        },
+        Subscription: {
+          newTodo: noop,
+        },
+      },
+    });
+
+    expect(console.warn).not.toBeCalled();
+  });
+
+  it("should warn if Mutation operations don't exist in the schema", function () {
+    new Store({
+      schema: require('../test-utils/simple_schema.json'),
+      updates: {
+        Mutation: {
+          doTheChaChaSlide: noop,
+        },
+      },
+    });
+
+    expect(console.warn).toBeCalledTimes(1);
+    const warnMessage = mocked(console.warn).mock.calls[0][0];
+    expect(warnMessage).toContain(
+      'Invalid mutation field: `doTheChaChaSlide` is not in the defined schema, but the `updates.Mutation` option is referencing it.'
+    );
+    expect(warnMessage).toContain('https://bit.ly/2XbVrpR#21');
+  });
+
+  it("should warn if Subscription operations don't exist in the schema", function () {
+    new Store({
+      schema: require('../test-utils/simple_schema.json'),
+      updates: {
+        Subscription: {
+          someoneDidTheChaChaSlide: noop,
+        },
+      },
+    });
+
+    expect(console.warn).toBeCalledTimes(1);
+    const warnMessage = mocked(console.warn).mock.calls[0][0];
+    expect(warnMessage).toContain(
+      'Invalid subscription field: `someoneDidTheChaChaSlide` is not in the defined schema, but the `updates.Subscription` option is referencing it.'
+    );
+    expect(warnMessage).toContain('https://bit.ly/2XbVrpR#22');
+  });
+});
+
 describe('Store with KeyingConfig', () => {
   it('generates keys from custom keying function', () => {
     const store = new Store({
@@ -138,6 +218,108 @@ describe('Store with KeyingConfig', () => {
   });
 });
 
+describe('Store with ResolverConfig', () => {
+  it("sets the store's resolvers field to the given argument", () => {
+    const resolversOption = {
+      Query: {
+        latestTodo: () => 'todo',
+      },
+    };
+
+    const store = new Store({
+      resolvers: resolversOption,
+    });
+
+    expect(store.resolvers).toBe(resolversOption);
+  });
+
+  it("sets the store's resolvers field to an empty default if not provided", () => {
+    const store = new Store({});
+
+    expect(store.resolvers).toEqual({});
+  });
+
+  it('should not warn if resolvers do exist in the schema', function () {
+    new Store({
+      schema: require('../test-utils/simple_schema.json'),
+      resolvers: {
+        Query: {
+          latestTodo: () => 'todo',
+          todos: () => ['todo 1', 'todo 2'],
+        },
+        Todo: {
+          text: todo => (todo.text as string).toUpperCase(),
+          author: todo => (todo.author as string).toUpperCase(),
+        },
+      },
+    });
+
+    expect(console.warn).not.toBeCalled();
+  });
+
+  it("should warn if a Query doesn't exist in the schema", function () {
+    new Store({
+      schema: require('../test-utils/simple_schema.json'),
+      resolvers: {
+        Query: {
+          todos: () => ['todo 1', 'todo 2'],
+          // This query should be warned about.
+          findDeletedTodos: () => ['todo 1', 'todo 2'],
+        },
+      },
+    });
+
+    expect(console.warn).toBeCalledTimes(1);
+    const warnMessage = mocked(console.warn).mock.calls[0][0];
+    expect(warnMessage).toContain(
+      'Invalid resolver: `Query.findDeletedTodos` is not in the defined schema, but the `resolvers` option is referencing it'
+    );
+    expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23');
+  });
+
+  it("should warn if a type doesn't exist in the schema", function () {
+    new Store({
+      schema: require('../test-utils/simple_schema.json'),
+      resolvers: {
+        Todo: {
+          complete: () => true,
+        },
+        // This type should be warned about.
+        Dinosaur: {
+          isExtinct: () => true,
+        },
+      },
+    });
+
+    expect(console.warn).toBeCalledTimes(1);
+    const warnMessage = mocked(console.warn).mock.calls[0][0];
+    expect(warnMessage).toContain(
+      'Invalid resolver: `Dinosaur` is not in the defined schema, but the `resolvers` option is referencing it'
+    );
+    expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23');
+  });
+
+  it("should warn if a type's property doesn't exist in the schema", function () {
+    new Store({
+      schema: require('../test-utils/simple_schema.json'),
+      resolvers: {
+        Todo: {
+          complete: () => true,
+          // This property should be warned about.
+          isAboutDinosaurs: () => true,
+        },
+      },
+    });
+
+    expect(console.warn).toBeCalledTimes(1);
+    const warnMessage = mocked(console.warn).mock.calls[0][0];
+    expect(warnMessage).toContain(
+      'Invalid resolver: `Todo.isAboutDinosaurs` is not in the defined schema, but the `resolvers` option is referencing it'
+    );
+    expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23');
+  });
+});
+
 describe('Store with OptimisticMutationConfig', () => {
   let store;
 
@@ -156,7 +338,7 @@ describe('Store with OptimisticMutationConfig', () => {
     InMemoryData.initDataState(store.data, null);
   });
 
-  it('Should resolve a property', () => {
+  it('should resolve a property', () => {
     const todoResult = store.resolve({ __typename: 'Todo', id: '0' }, 'text');
     expect(todoResult).toEqual('Go to the shops');
     const authorResult = store.resolve(
@@ -180,7 +362,7 @@ describe('Store with OptimisticMutationConfig', () => {
     InMemoryData.clearDataState();
   });
 
-  it('Should resolve a link property', () => {
+  it('should resolve a link property', () => {
     const parent = {
       id: '0',
       text: 'test',
@@ -659,4 +841,27 @@ describe('Store with storage', () => {
     expect(InMemoryData.readRecord('Query', 'base')).toBe(true);
     InMemoryData.clearDataState();
   });
+
+  it("should warn if an optimistic field doesn't exist in the schema's mutations", function () {
+    new Store({
+      schema: require('../test-utils/simple_schema.json'),
+      updates: {
+        Mutation: {
+          toggleTodo: noop,
+        },
+      },
+      optimistic: {
+        toggleTodo: () => null,
+        // This field should be warned about.
+        deleteTodo: () => null,
+      },
+    });
+
+    expect(console.warn).toBeCalledTimes(1);
+    const warnMessage = mocked(console.warn).mock.calls[0][0];
+    expect(warnMessage).toContain(
+      'Invalid optimistic mutation field: `deleteTodo` is not a mutation field in the defined schema, but the `optimistic` option is referencing it.'
+    );
+    expect(warnMessage).toContain('https://bit.ly/2XbVrpR#24');
+  });
 });
diff --git a/exchanges/graphcache/src/test-utils/simple_schema.json b/exchanges/graphcache/src/test-utils/simple_schema.json
index bf684f76..2f0e06c3 100644
--- a/exchanges/graphcache/src/test-utils/simple_schema.json
+++ b/exchanges/graphcache/src/test-utils/simple_schema.json
@@ -6,7 +6,9 @@
     "mutationType": {
       "name": "Mutation"
     },
-    "subscriptionType": null,
+    "subscriptionType": {
+      "name": "Subscription"
+    },
     "types": [
       {
         "kind": "OBJECT",
@@ -241,6 +243,27 @@
         "enumValues": null,
         "possibleTypes": null
       },
+      {
+        "kind": "OBJECT",
+        "name": "Subscription",
+        "fields": [
+          {
+            "name": "newTodo",
+            "args": [],
+            "type": {
+              "kind": "OBJECT",
+              "name": "Todo",
+              "ofType": null
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          }
+        ],
+        "inputFields": null,
+        "interfaces": [],
+        "enumValues": null,
+        "possibleTypes": null
+      },
       {
         "kind": "OBJECT",
         "name": "__Schema",
diff --git a/exchanges/graphcache/src/test-utils/utils.ts b/exchanges/graphcache/src/test-utils/utils.ts
new file mode 100644
index 00000000..b6c3f957
--- /dev/null
+++ b/exchanges/graphcache/src/test-utils/utils.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line
+export const noop = () => {};