Getting started / Tutorial

4. Testing your API

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.

It's at the system level

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 install --save-dev jest @types/jest ts-jest graphql-request get-port

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

Diff
Code
1"scripts": {
2 "dev": "ts-node-dev --transpile-only --no-notify api/index.ts",
3 "build": "tsc",
+ "generate": "ts-node --transpile-only api/schema",
+ "test": "npm run generate && jest"
6},
+"jest": {
+ "preset": "ts-jest",
+ "globals": {
+ "ts-jest": {
+ "diagnostics": { "warnOnly": true }
+ }
+ },
+ "testEnvironment": "node"
+}

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

To ease testing, we'll create a small utility that we'll call createTestContext, which is designed for running integration 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.

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

$touch tests/__helpers.ts
1// tests/__helpers.ts // 1
2import { ServerInfo } from "apollo-server";
3import getPort, { makeRange } from "get-port";
4import { GraphQLClient } from "graphql-request";
5import { server } from "../api/server";
6
7type TestContext = {
8 client: GraphQLClient;
9};
10
11export function createTestContext(): TestContext {
12 let ctx = {} as TestContext;
13 const graphqlCtx = graphqlTestContext();
14
15 beforeEach(async () => { // 2
16 const client = await graphqlCtx.before();
17
18 Object.assign(ctx, {
19 client,
20 });
21 });
22
23 afterEach(async () => { // 3
24 await graphqlCtx.after();
25 });
26
27 return ctx; // 8
28}
29
30function graphqlTestContext() {
31 let serverInstance: ServerInfo | null = null;
32
33 return {
34 async before() {
35 const port = await getPort({ port: makeRange(4000, 6000) }); // 4
36 serverInstance = await server.listen({ port }); // 5
37
38 return new GraphQLClient(`http://localhost:${port}`); // 6
39 },
40 async after() {
41 serverInstance?.server.close(); // 7
42 },
43 };
44}
  1. The module name prefix __ matches that of jest's for snapshot folders __snapshots__
  2. Make use of the jest lifecycle beforeEach. As the name suggest, this hook will be called by Jest before each of your tests
  3. Make use of the jest lifecycle afterEach. As the name suggest, this hook will be called by Jest after each of your tests
  4. Get a random port so that we can run multiple server concurrently. This is useful as Jest runs all the tests in a same file concurrently by default
  5. Start the GraphQL server before a test start
  6. Add a pre-configured GraphQL client to the test context so that each test can easily send queries to the spawned GraphQL server
  7. Stop the GraphQL server after each test is done
  8. Return an object containing a configured GraphQL client to easily send queries to your GraphQL 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.

Diff
Code
1// api/db.ts
2export interface Post {
3 id: number
4 title: string
5 body: string
6 published: boolean
7}
8
9export interface Db {
10 posts: Post[]
11}
12
13export const db = {
- posts: [{ id: 1, title: 'Nexus', body: '...', published: false }],
+ posts: []
16}

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.request(` # 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
23 // Publish the previously created draft
24 const publishResult = await ctx.client.request(`
25 mutation publishDraft($draftId: Int!) {
26 publish(draftId: $draftId) {
27 id
28 title
29 body
30 published
31 }
32 }
33 `,
34 { draftId: draftResult.createDraft.id }
35 )
36
37 // Snapshot the published draft and expect `published` to be true
38 expect(publishResult).toMatchInlineSnapshot()
39})
  1. The test context exposes a GraphQL client at ctx.client.request 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 snapshotted 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 true
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