Testing In Principal

There are multiple ways you can test a GraphQL API. One way is to extract resolvers into isolated functions and then unit test them. Of course these are rarely pure functions so those unit tests either become partial integration tests or mocks are introduced to try and retain the unit level of testing. Unit testing resolvers can work well, but here are some reasons why and where it might not;

  • The Nexus Prisma plugin (discussed later) can remove the need to even write resolvers in which case testing [those] resolvers doesn't make sense.
  • Thanks to the enhanced static type safety brought by Nexus, testing for correct handling of different input types and expected output types can be greatly reduced. For example you shouldn't need to test that your resolver checks for nulls before accessing nullable fields of an input object. And you don't need to test that your resolver is returning the right type.
  • Unit testing resolvers cannot provide a client perspective view of correctness since they focus on internals. If you want to test but have limited time/capacity to do so, you might choose to minimize/forgo the unit level in favor of system/integration tests that are closer to or at the level of a client perspective.

Testing non-trivial resolvers in isolation is likely to be a good investment in most cases but its up to you as a developer. What Nexus provides help with is not at this level, but higher up in the testing pyramid, at the system level. System testing means tests that will run operations against your API just like a real client would.

Note: This guide is written using jest because it is what we use internally and thus can speak to best. But you should be able to use your test framework of choice.

Testing in Nexus

Nexus comes with a special testing module that you can import from nexus/testing. Its primary utility is the createTestContext function. It is designed for running integration tests. When run it will in turn boot your app (in the same process) and expose an interface for your tests to interact with it.

For the curious...

Since jest runs test suites in parallel it means multiple instances of your app will be run in parallel too. The testing module takes care of abstracting the mechanics of making this work from you. For example it assigns random ports to each app to run its server and makes sure each test suite's app client is configured to be talking with its respective app instance. You should never have to think about these kinds of details though, and if it turns out you do please open a GitHub issue so we can try to seal the leak you've found in Nexus' abstraction!

Installing & configuring jest

Install jest and ts-jest

1npm install --save-dev jest ts-jest

Then configure jest to use ts-jest. Note: it's important that you set warnOnly option to true or Jest will prevent your tests from running in case of type errors.

1// jest.config.js
2module.exports = {
3 preset: 'ts-jest',
4 globals: {
5 'ts-jest': {
6 diagnostics: { warnOnly: true }
7 }
8 }
9}

A little helper

Before jumping into test suites we will wrap the createTestContext with a pattern that more tightly integrates it into jest. Nexus will probably ship something like as follows or better in the future, but for now you can copy this into your projects:

1// tests/__helpers.ts
2import { createTestContext as originalCreateTestContext, TestContext } from 'nexus/testing'
3
4export function createTestContext(): TestContext {
5 let ctx = {} as TestContext
6
7 beforeAll(async () => {
8 Object.assign(ctx, await originalCreateTestContext())
9 await ctx.app.start()
10 })
11
12 afterAll(async () => {
13 await ctx.app.stop()
14 })
15
16 return ctx
17}

We'll use this in other test suites roughly like so:

1// tests/foo.spec.ts
2import { createTestContext } from './__helpers'
3
4const ctx = createTestContext()
5
6it('foo', () => {
7 // use `ctx` in here
8})

Removing boilerplate away from your test code is a win and staying DRY about it across multiple test suites helps. But do note that ctx is not usable outside of jest blocks (it before after ...). If you try to you'll find it to be undefined.

1import { createTestContext } from './__helpers'
2
3const { app } = createTestContext() // Error!

Test context interface

The Test context interface is extensible by plugins. By default it includes basic control over the app and a GraphQL client instance ready to send queries and mutations to it.

Without a database

Example

1import { createTestContext } from './__helpers'
2
3const ctx = createTestContext()
4
5it('makes sure a user was registered', async () => {
6 // ctx.client.send sends requests to your locally running nexus server
7 const result = await ctx.client.send(`
8 mutation {
9 signupUser(data: { email: "person@email.com", password: "123456" })
10 } {
11 id
12 email
13 password
14 }
15 `)
16
17 const createdUsers = await ctx.client.send(`{ users { id } }`)
18 expect(createdUsers).toMatchSnapshot()
19})

With a database

Integration testing with a database can add a lot of complexity to your test suite. But Nexus is in a good position to help since it knows about both test and database domains of your app.

Integration between Nexus' test and database systems is young and still missing many features. Below we will cover some utilities and patterns that you can copy into your project meanwhile.

Note: This assumes you have setup a PostgreSQL database running locally. You could use any database supported by Prisma though.

  1. Install new development dependencies for upcoming test utilities.

    $npm add --save-dev nanoid pg jest-environment-node
  2. Create a specialized "jest environment" that will manage a real database for your tests to run against.

    1// nexus-test-environment.js
    2const { Client } = require('pg')
    3const NodeEnvironment = require('jest-environment-node')
    4const { nanoid } = require('nanoid')
    5const util = require('util')
    6const exec = util.promisify(require('child_process').exec)
    7
    8const prismaBinary = './node_modules/.bin/prisma'
    9
    10/**
    11 * Custom test environment for nexus and Postgres
    12 */
    13class PrismaTestEnvironment extends NodeEnvironment {
    14 constructor(config) {
    15 super(config)
    16
    17 // Generate a unique schema identifier for this test context
    18 this.schema = `test_${nanoid()}`
    19
    20 // Generate the pg connection string for the test schema
    21 this.connectionString = `postgres://postgres:postgres@localhost:5432/testing?schema=${this.schema}`
    22 }
    23
    24 async setup() {
    25 // Set the required environment variable to contain the connection string
    26 // to our database test schema
    27 process.env.POSTGRES_URL = this.connectionString
    28 this.global.process.env.POSTGRES_URL = this.connectionString
    29
    30 // Run the migrations to ensure our schema has the required structure
    31 await exec(`${prismaBinary} migrate up --experimental`)
    32
    33 return super.setup()
    34 }
    35
    36 async teardown() {
    37 // Drop the schema after the tests have completed
    38 const client = new Client({
    39 connectionString: this.connectionString,
    40 })
    41 await client.connect()
    42 await client.query(`DROP SCHEMA IF EXISTS "${this.schema}" CASCADE`)
    43 await client.end()
    44 }
    45}
    46
    47module.exports = PrismaTestEnvironment
  3. Update your jest config to use your new test environment.

    ++ jest.config.ts
    2const { join } = require('path')
    3
    4module.exports = {
    5 preset: 'ts-jest',
    6 globals: {
    7 'ts-jest': {
    8 diagnostics: { warnOnly: true }
    9 }
    10 },
    11 rootDir: 'tests',
    + testEnvironment: join(__dirname, 'nexus-test-environment.js'),
    13}
  4. Edit your schema.prisma file to use an environment variable.

    ++ schema.prisma
    2datasource db {
    3 provider = "postgresql"
    - url = "postgresql://..."
    + url = env("POSTGRES_URL")
    6}
  5. Create a .env file at the root of your project directory and add the following.

    1POSTGRES_URL="<your-development-postgres-url>"
  6. nexus-plugin-prisma augment TestContext['app'] with a db property. This can be used for example to seed your database with data at the beginning of a test suite:

    1beforeAll(async () => {
    2 await ctx.app.db.users.createOne({ ... })
    3})
  7. For now, just update the createTestContext wrapper to integrate your app's db client:

    ++ nexus-test-environment.js
    2afterAll(async () => {
    3 await ctx.app.server.stop()
    + await ctx.app.db.client.disconnect()
    5})
  8. That's it. Despite adding a database to your integration tests, they are essentially no more complex than without a database, which is great. Of course they will run a bit slower now.

    We will cover seeding your test database with data in in the future iteration of this guide.

    1// tests/user.test.ts
    2
    3import { createTestContext } from './__helpers'
    4
    5const ctx = createTestContext()
    6
    7it('makes sure a user was registered', async () => {
    8 // ctx.client.send sends requests to your locally running nexus server
    9 const result = await ctx.client.send(`
    10 mutation {
    11 signupUser(data: { email: "person@email.com", password: "123456" })
    12 } {
    13 id
    14 email
    15 password
    16 }
    17 `)
    18
    19 const createdUsers = await ctx.client.send(`{ users { id } }`)
    20 expect(createdUsers).toMatchSnapshot()
    21})
Edit this page on Github