Typesafe Unit Testing of GraphQL Resolvers in TypeScript

Published on 9 October 2020

This post gives a code example on how to unit test a GraphQL resolver with Jest in TypeScript. In order to make the testing typesafe, we use GraphQL Code Generator.

Description

Unit testing GraphQL resolvers in TypeScript can be quite tricky to get right with the correct typing. This posts illustrates some example code of setting up calls directly to the resolvers.

The code below here works with GraphQL Code Generator to generate types for your resolvers. This article does not cover how to set up GraphQL Code Generator; it assumes that you have a working setup with your resolvers.

Instead of writing raw GraphQL queries (which can be seen in my post about testing middlewares), we show a typesafe way to unit test GraphQL resolvers. Much of this code is based off what was written in graphql-code-generator issue #1763 by kamilkisiela.

Code

// You need to set up your resolvers using GraphQL Code Generator types. This
// isn't in the scope of this article, so you'll have to check its
// documentation.

// You probably want to put these helpers in a common file such as testutils.ts
// in order to use them between different test suites. You don't really need to
// read the code below.
//
// The types below help us properly type mockedResolvers so we can write unit
// tests that directly call resolvers from graphql-codegen. I took it from this
// link below.
// https://github.com/dotansimha/graphql-code-generator/issues/1763
type MockResolverFn<TResult, TParent, TArgs, TCtx> = (
  parent?: TParent,
  args?: TArgs,
  context?: TCtx,
  info?: any
) => Promise<TResult> | TResult;

type MockStitchingResolver<TResult, TParent, TArgs, TCtx> = {
  fragment: string;
  resolve: MockResolverFn<TResult, TParent, TArgs, TCtx>;
};

export function mockResolver<TResult, TParent, TContext, TArgs>(
  resolver: Resolver<TResult, TParent, TContext, TArgs>
): MockResolverFn<TResult, TParent, TArgs, TContext>;
export function mockResolver<TResult, TParent, TContext, TArgs>(
  resolver: Resolver<TResult, TParent, TContext, TArgs>,
  isStitching: boolean
): MockStitchingResolver<TResult, TParent, TArgs, TContext>;
export function mockResolver<TResult, TParent, TContext, TArgs>(
  resolver: Resolver<TResult, TParent, TContext, TArgs>,
  useStitching?: boolean
):
  | MockResolverFn<TResult, TParent, TArgs, TContext>
  | MockStitchingResolver<TResult, TParent, TArgs, TContext> {
  if (
    isStitching<StitchingResolver<TResult, TParent, TContext, TArgs>>(
      resolver,
      useStitching || false
    )
  ) {
    return {
      fragment: resolver.fragment,
      resolve: (parent, args, context, info) =>
        resolver.resolve(parent!, args!, context!, info!),
    };
  }

  return (parent, args, context, info) =>
    resolver(parent!, args!, context!, info!);
}

function isStitching<T>(resolver: any, useStitching: boolean): resolver is T {
  return useStitching === true;
}


/**
 * In this example, we have an object called `signup` that has the
 * `MutationResolvers` type from graphql-codegen.
 */
import { signup } from './signup';

// Set up the parent and context args to your resolvers as appropriate, either
// at the top or on a test-by-test basis.
const RESOLVER_PARENT = {};
const MOCK_CONTEXT = {};

describe('cancel signup works', () => {
  it('tests for an exception case', async () => {
    const expectToThrow = async () => {
      return mockResolver(signup.cancelSignup!)(
        RESOLVER_PARENT,
        {
          reason: 'foo',
        },
        MOCK_CONTEXT
      );
    };
    expect.assertions(1);
    return expectToThrow().catch((e) => {
      expect(e).toBeDefined();
    });
  });

  it('tests the return value of the resolver', async () => {
    // You can also set up other mocks as needed, then assert that they were
    // called later on.
    const result = await mockResolver(signup.cancelSignup!)(
      RESOLVER_PARENT,
      {
        reason: 'foo',
      },
      MOCK_CONTEXT
    );

    expect(result.someFieldOnReturnResult).toBe("some test assertion");
  });
});

Comments