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.
$
Then, configure jest and npm scripts in your package.json
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
$
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 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.
$
1// tests/__helpers.ts // 12import { ServerInfo } from "apollo-server";3import getPort, { makeRange } from "get-port";4import { GraphQLClient } from "graphql-request";5import { server } from "../api/server";67type TestContext = {8 client: GraphQLClient;9};1011export function createTestContext(): TestContext {12 let ctx = {} as TestContext;13 const graphqlCtx = graphqlTestContext();1415 beforeEach(async () => { // 216 const client = await graphqlCtx.before();1718 Object.assign(ctx, {19 client,20 });21 });2223 afterEach(async () => { // 324 await graphqlCtx.after();25 });2627 return ctx; // 828}2930function graphqlTestContext() {31 let serverInstance: ServerInfo | null = null;3233 return {34 async before() {35 const port = await getPort({ port: makeRange(4000, 6000) }); // 436 serverInstance = await server.listen({ port }); // 53738 return new GraphQLClient(`http://localhost:${port}`); // 639 },40 async after() {41 serverInstance?.server.close(); // 742 },43 };44}
- The module name prefix
__
matches that of jest's for snapshot folders__snapshots__
- Make use of the
jest
lifecyclebeforeEach
. As the name suggest, this hook will be called by Jest before each of your tests - Make use of the
jest
lifecycleafterEach
. As the name suggest, this hook will be called by Jest after each of your tests - 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
- Start the GraphQL server before a test start
- Add a pre-configured GraphQL client to the test context so that each test can easily send queries to the spawned GraphQL server
- Stop the GraphQL server after each test is done
- 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.
1// api/db.ts2export interface Post {3 id: number4 title: string5 body: string6 published: boolean7}89export interface Db {10 posts: Post[]11}1213export 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.ts23import { createTestContext } from './__helpers'45const ctx = createTestContext()67it('ensures that a draft can be created and published', async () => {8 // Create a new draft9 const draftResult = await ctx.client.request(` # 110 mutation {11 createDraft(title: "Nexus", body: "...") { # 212 id13 title14 body15 published16 }17 }18 `)1920 // Snapshot that draft and expect `published` to be false21 expect(draftResult).toMatchInlineSnapshot() // 32223 // Publish the previously created draft24 const publishResult = await ctx.client.request(`25 mutation publishDraft($draftId: Int!) {26 publish(draftId: $draftId) {27 id28 title29 body30 published31 }32 }33 `,34 { draftId: draftResult.createDraft.id }35 )3637 // Snapshot the published draft and expect `published` to be true38 expect(publishResult).toMatchInlineSnapshot()39})
- 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. - This is the mutation from the end of last chapter.
- 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:
$
Draft snapshot
1// Snapshot that draft and expect `published` to be false2expect(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 true2expect(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!