graphql-nexus / nexus

Code-First, Type-Safe, GraphQL Schema Construction
https://nexusjs.org
MIT License
3.4k stars 275 forks source link

Testing nexus resolvers #115

Open spencerolsen opened 5 years ago

spencerolsen commented 5 years ago

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?

tgriesser commented 5 years ago

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.

danielhusar commented 4 years ago

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

fullStackDataSolutions commented 4 years ago

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.

tgriesser commented 4 years ago

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.

fullStackDataSolutions commented 4 years ago

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();
        });
    });
});
tgriesser commented 4 years ago

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.

iddan commented 3 years ago

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

tom-sherman commented 3 years ago

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?

lucashfreitas commented 2 years ago

@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?