Overview

So far you've been validating your work by manual interacting with the Playground. That might be reasonable at first (depending on your relationship to TDD) but it will not scale. At some point you are going to want automated testing. So in this chapter you're going to add some automated tests to your e-commerce project. You'll learn about:

  • Nexus' approach to testing
  • Setting up a test environment
  • The nexus/testing module

It's at the system level

Content taken from the testing guide

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. This chapter will focus on that. Let's dive-in!

Setting up your test environment

During this tutorial, you'll use the Jest testing framework to test your API. This is not mandatory but we do recommend it. Still, in general, outside this tutorial, if you prefer another testing framework, feel free to use it.

First, install jest and accompanying tools

$npm add --save-dev jest @types/jest ts-jest

Then, configure jest and npm scripts in your package.json

1"scripts": {
2 "test": "jest"
3},
4"jest": {
5 "preset": "ts-jest",
6 "globals": {
7 "ts-jest": {
8 "diagnostics": { "warnOnly": true }
9 }
10 },
11 "testEnvironment": "node"
12}

Finally, create a tests folder at the root of your project and a Post.test.ts file inside it

$mkdir tests && touch tests/Post.test.ts

Testing the publish mutation

Nexus comes with a special testing module that you can import from nexus/testing.

Its primary export is the createTestContext function which is designed for running system tests. When run, it will boot your app in the same process as the test suite and expose an interface for your tests to interact with it. Jest runs each test suite in its own process, so if you have have say eight test suites running in parallel that means you'll have eight app processes running too.

Before jumping into your first test we will show you a pattern that more tightly integrates createTestContext into jest. Nexus will probably ship something like as follows or better in the future, but for now you can just add this as test helper in your Nexus projects.

Create a tests/__helpers.ts module with the following contents.

$touch tests/__helpers.ts
1// tests/__helpers.ts // 1
2
3import { createTestContext as originalCreateTestContext, TestContext } from 'nexus/testing'
4
5export function createTestContext() {
6 let ctx = {} as TestContext // 2
7
8 beforeAll(async () => {
9 Object.assign(ctx, await originalCreateTestContext()) // 3
10 await ctx.app.start() // 4
11 })
12
13 afterAll(async () => {
14 await ctx.app.stop() // 5
15 })
16
17 return ctx
18}
  1. The module name prefix __ matches that of jest's for snapshot folders __snapshots__
  2. Create an object that will be returned immediately but mutated before tests run
  3. Before tests run create the test context. This does most of the work like getting your app instance.
  4. Before tests run start the app
  5. After tests complete, stop the app. This will for example close your app's HTTP server.

Alright, now you will test your publish mutation. Because we want to start from a clean database, we'll just remove the pre-seeded data in the in-memory database that we've been using up until now.

1// api/db.ts
2export const db = {
- posts: [{ id: 1, title: 'Nexus', body: '...', published: false }],
+ posts: []
5}

Now use your new helper and scaffold your first test:

1// tests/Post.test.ts
2
3import { createTestContext } from './__helpers'
4
5const ctx = createTestContext()
6
7it('ensures that a draft can be created and published', async () => {
8 // Create a new draft
9 const draftResult = await ctx.client.send(` # 1
10 mutation {
11 createDraft(title: "Nexus", body: "...") { # 2
12 id
13 title
14 body
15 published
16 }
17 }
18 `)
19
20 // Snapshot that draft and expect `published` to be false
21 expect(draftResult).toMatchInlineSnapshot(` // 3
22 Object {
23 "createDraft": Object {
24 "body": "...",
25 "id": 1,
26 "published": false,
27 "title": "Nexus",
28 },
29 }
30 `)
31
32 // Publish the previously created draft
33 const publishResult = await ctx.client.send(`
34 mutation publishDraft($draftId: Int!) {
35 publish(draftId: $draftId) {
36 id
37 title
38 body
39 published
40 }
41 }
42 `,
43 { draftId: draftResult.createDraft.id }
44 )
45
46 // Snapshot the published draft and expect `published` to be true
47 expect(publishResult).toMatchInlineSnapshot(`
48 Object {
49 "publish": Object {
50 "body": "...",
51 "id": 1,
52 "published": true,
53 "title": "Nexus",
54 },
55 }
56 `)
57})
  1. The test context exposes a GraphQL client at ctx.client.send that will help us run operations against our API. Here We're using it to send a publish mutation.
  2. This is the mutation from the end of last chapter.
  3. The result will be snapshoted inline allowing us to see the input and output collocated!

Try it out

Now run your tests and let's see the snapshots come to life! It should look similar to this:

$npm run test

Draft snapshot

1// Snapshot that draft and expect `published` to be false
2expect(result).toMatchInlineSnapshot(`
+ Object {
+ "publish": Object {
+ "id": 1,
+ "title": "Nexus",
+ "body": "...",
+ "published": false,
+ },
+ }
11 `)

Published draft snapshot

1// Snapshot that draft and expect `published` to be false
2expect(result).toMatchInlineSnapshot(`
+ Object {
+ "publish": Object {
+ "id": 1,
+ "title": "Nexus",
+ "body": "...",
+ "published": true,
+ },
+ }
11 `)

Awesome, beautiful workflow isn't it? If inline snapshots get too unwieldy you can switch to regular snapshots and install a VSCode plugin that will display the snapshots upon hovering over the toMatchSnapshot method name. While not quite as fluid as seeing inline snapshots throughout your test module, it may work better for you.

Wrapping up

You've just made a big step in the maintainability of your API. Here we showed how to test your createDraft and publish mutation, proving that a draft can be properly created and published. However you did not test if the draft was correctly persisted into your database. That piece will come soon! But first we need a real database in the first place. That's what the next chapter is all about!

Next →
Edit this page on Github