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
1
Then, head to your tests/__helpers.ts
file to add the following imports and 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";1112type TestContext = {13 client: GraphQLClient;+ db: PrismaClient;15};1617export function createTestContext(): TestContext {18 let ctx = {} as TestContext;19 const graphqlCtx = graphqlTestContext();+ const prismaCtx = prismaTestContext();2122 beforeEach(async () => {23 const client = await graphqlCtx.before();+ const db = await prismaCtx.before();2526 Object.assign(ctx, {27 client,+ db,29 });30 });3132 afterEach(async () => {33 await graphqlCtx.after();+ await prismaCtx.after();35 });3637 return ctx;38}3940function graphqlTestContext() {41 let serverInstance: ServerInfo | null = null;4243 return {44 async before() {45 const port = await getPort({ port: makeRange(4000, 6000) });4647 serverInstance = await server.listen({ port });+ // Close the Prisma Client connection when the Apollo Server is closed+ serverInstance.server.on("close", async () => {+ db.$disconnect()+ });5253 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:
- Connect to an in-memory instance of the SQLite database
- Pushes the Prisma Schema to the database
- Generates a new Prisma Client
- 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.ts23it('ensures that a draft can be created and published', async () => {4 // ...56 // Publish the previously created draft7 const publishResult = await ctx.client.request(8 `9 mutation publishDraft($draftId: Int!) {10 publish(draftId: $draftId) {11 id12 title13 body14 published15 }16 }17 `,18 { draftId: draftResult.createDraft.id }19 )2021 // Snapshot the published draft and expect `published` to be true22 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!