Getting started / Tutorial

6. Testing with Prisma

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. 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 test:

  • Connect to a SQLite 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 SQLite database
  • Run your test
  • Teardown the schema entirely

Setting up the environment

To achieve some of the steps described above, we'll tweak our test context.

First, install the sqlite3 and nanoid packages

1npm install --save-dev sqlite3 @types/sqlite3

Then, head to your tests/__helpers.ts file to add the following imports and code

Diff
Code
1// tests/__helpers.ts
+import { PrismaClient } from "@prisma/client";
3import { ServerInfo } from "apollo-server";
+import { execSync } from "child_process";
5import getPort, { makeRange } from "get-port";
6import { GraphQLClient } from "graphql-request";
+import { join } from "path";
+import { Database } from "sqlite3";
+import { db } from "../api/db";
10import { server } from "../api/server";
11
12type TestContext = {
13 client: GraphQLClient;
+ db: PrismaClient;
15};
16
17export function createTestContext(): TestContext {
18 let ctx = {} as TestContext;
19 const graphqlCtx = graphqlTestContext();
+ const prismaCtx = prismaTestContext();
21
22 beforeEach(async () => {
23 const client = await graphqlCtx.before();
+ const db = await prismaCtx.before();
25
26 Object.assign(ctx, {
27 client,
+ db,
29 });
30 });
31
32 afterEach(async () => {
33 await graphqlCtx.after();
+ await prismaCtx.after();
35 });
36
37 return ctx;
38}
39
40function graphqlTestContext() {
41 let serverInstance: ServerInfo | null = null;
42
43 return {
44 async before() {
45 const port = await getPort({ port: makeRange(4000, 6000) });
46
47 serverInstance = await server.listen({ port });
+ // Close the Prisma Client connection when the Apollo Server is closed
+ serverInstance.server.on("close", async () => {
+ db.$disconnect()
+ });
52
53 return new GraphQLClient(`http://localhost:${port}`);
54 },
55 async after() {
56 serverInstance?.server.close();
57 },
58 };
59}
60
+function prismaTestContext() {
+ const prismaBinary = join(__dirname, "..", "node_modules", ".bin", "prisma");
+ let prismaClient: null | PrismaClient = null;
+
+ return {
+ async before() {
+
+ // Run the migrations to ensure our schema has the required structure
+ execSync(`${prismaBinary} db push --preview-feature`,);
+
+ // Construct a new Prisma Client connected to the generated schema
+ prismaClient = new PrismaClient();
+
+ return prismaClient;
+ },
+ async after() {
+ // Drop the schema after the tests have completed
+ const client = new Database(':memory:');
79
+ await client.close();
+
+ // Release the Prisma Client connection
+ await prismaClient?.$disconnect();
+ },
+ };
+ }

The prismaTestContext is in charge of a couple of things:

  1. Connect to an in-memory instance of the SQLite database
  2. Pushes the Prisma Schema to the database
  3. Generates a new Prisma Client
  4. Add an instance of a Prisma Client connected to the schema specifically for the test

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.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.request(
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.db.post.findMany()
34
+ expect(persistedData).toMatchInlineSnapshot()
36})

Now run your tests

$npm run test

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.

This is the end of the tutorial. Thanks for trying it out and please share any feedback you have with us!

Edit this page on Github