Chapter 6: Testing With Prisma

There's a couple of things you'll have to do in order to run integration tests against your API now that it's connected to a real development database. Please note that a lot of the following steps will most likely be simplified in the future. We're just not there yet. In this chapter, you'll learn about:

  • Custom Jest environment
  • Integration test with a real database

How does it work?

To perform integration testing against a real database, here are the high level steps we will follow for every tests:

  • Connect to a Postgres database. Most likely your dev database.
  • Migrate our database schema to a randomly generated schema of that database. This ensures that every tests runs from a clean un-seeded database
  • Make the Prisma Client connect to that Postgres schema
  • Run your test
  • Teardown the schema entirely

Setting up the environment

To achieve some of the steps described above, we'll use a custom Jest environment.

First, install pg and nanoid packages

1npm add pg nanoid

Create a tests/nexus-test-environment.js module and copy & paste the following to it

1// tests/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, Prisma 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.databaseUrl = `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.DATABASE_URL = this.databaseUrl
28 this.global.process.env.DATABASE_URL = this.databaseUrl
29
30 // Run the migrations to ensure our schema has the required structure
31 await exec(`${prismaBinary} migrate up --create-db --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.databaseUrl,
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

Make sure that the databaseUrl property has the right credentials to connect to your own database. Leave the /testing?schema=... part though. This ensures that your tests will add data to your Postgres instance in a separate database called testing in a schema that randomly generated.

Then, configure Jest to use that custom environment in your package.json

1"jest": {
2 "preset": "ts-jest",
3 "globals": {
4 "ts-jest": {
5 "diagnostics": { "warnOnly": true }
6 }
7 },
- "testEnvironment": "node",
+ "testEnvironment": "./tests/nexus-test-environment.js"
10}

Finally, thanks to the nexus-plugin-prisma, the test context that we previously used should now be augmented with a ctx.app.db property. That db property holds an instance of the Prisma Client to give you access to the underlying testing database. This is useful, for instance, to seed your database or make sure that some data was properly inserted.

The last thing we need to do to setup our environment is to make sure that we properly close the database connection after all tests. To do that, head to your tests/__helpers.ts module and add the following

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

Updating our test

We're ready to update our test so that it uses our database. Wait though. Is there even something to change? No, absolutely nothing. In fact, you can already try running Jest again and your test should pass. That's precisely the point of integration tests.

There's one thing we can do though. If you remember our previous test, the only part we couldn't test was whether or not the data had properly been persisted into the database.

Let's use the ctx.app.db property to fetch our database right after we've published the draft to ensure that it's been created by snapshotting the result.

1// tests/Post.test.ts
2
3it('ensures that a draft can be created and published', async () => {
4 // ...
5
6 // Publish the previously created draft
7 const publishResult = await ctx.client.send(
8 `
9 mutation publishDraft($draftId: Int!) {
10 publish(draftId: $draftId) {
11 id
12 title
13 body
14 published
15 }
16 }
17 `,
18 { draftId: draftResult.createDraft.id }
19 )
20
21 // Snapshot the published draft and expect `published` to be true
22 expect(publishResult).toMatchInlineSnapshot(`
23 Object {
24 "publish": Object {
25 "body": "...",
26 "id": 1,
27 "published": true,
28 "title": "Nexus",
29 },
30 }
31 `)
32
+ const persistedData = await ctx.app.db.client.post.findMany()
34
+ expect(persistedData).toMatchInlineSnapshot()
36})

The new snapshot should look like the following. It proves that our database did persist that data and that we have exactly one item in it.

1expect(persistedData).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "body": "...",
+ "id": 1,
+ "published": true,
+ "title": "Nexus",
+ },
+ ]
10 `)

Wrapping up

Congrats, you've performed your first real-world integration test. The fact that integration tests are completely decoupled from the implementation of your GraphQL API makes it a lot easier to maintain your test suite as you evolve your API. What matters is only the data that it produces, which also helps you cover your app a lot more than a single unit test.

About our app, it's starting to take shape but it's still lacking something pretty important in any application: authentication.

Come onto the next chapter to get that added to your app!

Next →
Edit this page on Github