nexus icon indicating copy to clipboard operation
nexus copied to clipboard

Testing nexus resolvers

Open spencerolsen opened this issue 5 years ago • 9 comments

What is the recommended method for testing a custom resolver with nexus? Currently the below code is harder for us to test, so we ended up passing the call to our previously existing resolvers that were easier to test.

export const mutation = prismaObjectType({
    t.list.field('upsertRoleOverrides', {
        type: 'RoleOverride',
        deprecation: "Use upsertNavigationItemOverrides once renaming of RoleOverride -> NavigationItemOverride.",
        description: "Create or update many RoleOverrides. Provide the id for the RoleOverrides that are know to already exist, leave it out if creating a new RoleOverride.",
        args: {
          data: arg({ type: 'RoleOverrideInput', list: true, required: true})
        },
        resolve: async(root, args, ctx, info) => {
          return (<any>roleOverrideResolver).Mutation.upsertRoleOverrides(root, args);
        }
    });
});

Our project is nodejs/typescript and as much as possible we try to write classes in order to make testing easier. What are best/recommended practices?

spencerolsen avatar Apr 16 '19 17:04 spencerolsen

My current testing approach involves executing an integration style test against the GraphQL query, and then observing/mocking various pieces of the context as necessary.

I've written a custom graphql-code-generator template that I'm hoping to clean up to be able to open-source to help with this.

It takes "test case" graphql files and turns them into self contained functions that have type completion on the arguments and execute queries against the graphql endpoint.

So given this .graphql file:

query Test_project($projectId: String!) {
  project(id: $projectId) {
    id
  }
}

This would be an example use in a test case:

test('missing project', () => {
   const response = await testProjectQuery({projectId: 'some-missing-project'})
   expect(result.body.data).toEqual(null)
   expect(result.body.errors).toEqual([
      {
        extensions: { code: 'INTERNAL_SERVER_ERROR' },
        locations: [{ column: 3, line: 2 }],
        message: 'INTERNAL_SERVER_ERROR',
        path: ['project'],
      },
    ])
})

Here's an abbreviated snippet of what the template/generated code looks like:

export type Test_ProjectQueryVariables = {
  projectId: Scalars['String']
}
export type Test_ProjectQuery = { __typename?: 'Query' } & {
  project: { __typename?: 'Project' } & Pick<Project, 'id'>
}
const ENDPOINT = '/graphql'
import gql from 'graphql-tag'
import { app as TestApp } from '@packages/api-graphql'
import supertest from 'supertest'
import {
  DocumentNode,
  OperationDefinitionNode,
  print,
  GraphQLFormattedError,
} from 'graphql'

type GraphQLTestCaseResult<ResultType> = {
  status: number
  body: {
    data: ResultType | null
    errors: GraphQLFormattedError[]
  }
}

function makeGraphqlTestCase<ResultType, Variables>(document: DocumentNode) {
  return async (
    variables: Variables
  ): Promise<GraphQLTestCaseResult<ResultType>> => {
    const operationNode = document.definitions[0] as OperationDefinitionNode
    const body = {
      query: print(document),
      variables,
      operationName: operationNode.name ? operationNode.name.value : null,
    }
    return (
      supertest(TestApp)
        .post(ENDPOINT)
        .send(body)
        .set('Accept', 'application/json')
        .catch((e) => {
          if (e.response) {
            console.error(e.response.text)
          } else {
            console.error(e)
          }
          throw e
        })
        .then((res) => {
          const result = {
            status: res.status,
            body: res.body,
          }
          return result
        })
    )
  }
}
export const Test_ProjectDocument = gql`
  query Test_project($projectId: String!) {
    project(id: $projectId) {
      id
    }
  }
`
export const testProjectQuery = makeGraphqlTestCase<
  Test_ProjectQuery,
  Test_ProjectQueryVariables
>(Test_ProjectDocument)

I'm pairing this with other utilities I've written that help me mock/intercept pieces of the request/response more granularly. Though it sounds like the approach of just calling out to your already tested resolve fns would be a good approach as well.

tgriesser avatar Apr 18 '19 21:04 tgriesser

It would be great to have official docs and examples on testing resolvers.

danielhusar avatar Mar 08 '20 05:03 danielhusar

I'm trying to test my Nexus API, and have been looking or hours on ways to do this. We have existing tests from our old schema first API that uses makeExecutableSchema() to test, but I see no way of use makeExecutableSchema() with Nexus. Although it does seem possible, just can't find any documentation on how to actually do it.

fullStackDataSolutions avatar Mar 26 '20 17:03 fullStackDataSolutions

We have existing tests from our old schema first API that uses makeExecutableSchema() to test

makeExecutableSchema just constructs a GraphQLSchema object.

makeSchema in Nexus returns the same GraphQLSchema, so however you were using the schema from graphql-tools should work the same with Nexus.

tgriesser avatar Mar 27 '20 13:03 tgriesser

Unfortunately, the Schema returned from makeSchema doesn't work I did find that this works:

const testSchema = makeExecutableSchema({
    typeDefs: importSchema(path.join(__dirname, '../generated/schema.graphql')),
    resolvers: resolvers
});

But it only works because we haven't yet moved the resolvers into the Nexus files, and have them separately, which defeats the point of Nexus as we have to maintain to file structures if we don't move them in, also there's other reason we want to get rid of the separate resolvers.

So the real question I guess is:

  1. How do you pull the resolvers from Nexus to use in this fashion?
  2. or how to I use the Schema returned by makeSchema to test?

Here's one of my tests for reference:

import { addMockFunctionsToSchema } from 'graphql-tools';
import { graphql } from 'graphql';
import { testSchema as schema } from '../schema';

describe('Query Tests: getSomething', () => {
    test('getSomething', done => {
        const data = {
            state: 'CA',
            number: 1,
            year: 2020,
        };

        const mocks = {
            something: () => ({
                state: () => data.state,
                number: () => data.number,
                year: () => data.year
            })
        };

        addMockFunctionsToSchema({ schema, mocks });

        const query = `
            query {
                getSomething(id: 1) {
                    state
                    number
                    year
                }
            }
        `;

        graphql(schema, query).then(result => {
            expect(result.data.getSomething).toEqual(data);
            done();
        });
    });
});

fullStackDataSolutions avatar Mar 27 '20 17:03 fullStackDataSolutions

Assuming you're testing the server implementation, if you're mocking all the resolvers then it isn't actually testing anything meaningful other than that the execution of graphql-js functions properly.

All tests I'll typically write are integration level tests, meaning the actual resolvers are executed, and any mocks are dealt with at either the network layer: https://github.com/nock/nock, by stubbing/mocking actual object methods at the context layer, or by pre-seeding a database with expected results.

tgriesser avatar Mar 27 '20 23:03 tgriesser

Since nexus uses Apollo Server you can use Apollo Server testing. See Integration testing. A tutorial can be added that follow Apollo tutorial but uses Nexus schema

iddan avatar Apr 12 '21 17:04 iddan

A doc exists that recommends best practice with testing a Nexus schema https://nexusjs.org/docs/getting-started/tutorial/chapter-testing-your-api

Can this issue be closed?

tom-sherman avatar May 24 '21 21:05 tom-sherman

@tgriesser does nexus offer any typescript helpers to infer the resolver function types based on field types and args? Currently, the only way I can get my resolvers with type safety is to use them within field definition.

Due to this, all my resolvers are tightened with the schema definition (due to typescript typesafety) and therefore can not be tested separately, without a test graphql server/integration test.

I have raised the same question in this issue: https://github.com/graphql-nexus/nexus/issues/1012

Would that be considered as a pull request? I believe would give more flexibility to Nexus?

lucashfreitas avatar Nov 14 '21 19:11 lucashfreitas