Testing
When building a web application it's important to be able to test the behaviour of your system to ensure it does what you expect. In this guide we'll show you how to use @keystone-6/core/testing and Vitest to write tests that check the behaviour of your GraphQL API.
What tests might look like
Typically you will use tests to verify the behaviour of your code. You might typically test things like your access control, hooks, virtual fields et cetera.
The provider-specific resetDatabase helpers reset the database and then apply the migration.sql files from the migrations directory in order. They create the configured database if it does not exist. They do not read schema.prisma or run Prisma db push.
Before using it, make sure your migrations are up to date with your schema:
keystone build --no-uiprisma migrate dev
Running keystone dev by itself is not enough because db push does not create migration files. See the database migration guide for the complete migration workflow.
import path from 'node:path'import { getContext } from '@keystone-6/core/context'import { resetDatabase } from '@keystone-6/core/testing/sqlite'import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'import { afterAll, beforeEach, expect, test } from 'vitest'import * as PrismaModule from './generated/prisma/client'import baseConfig from './keystone'const dbPath = `./test-${process.env.VITEST_WORKER_ID}.db`const dbUrl = `file:${dbPath}`const config = {...baseConfig,db: {...baseConfig.db,prismaClientOptions: () => ({ adapter: new PrismaBetterSqlite3({ url: dbUrl }) }),},}beforeEach(async () => {const migrationsDirectory = path.join(__dirname, 'migrations')await resetDatabase({ filename: dbPath }, migrationsDirectory)})const context = getContext(config, PrismaModule)afterAll(() => context.prisma.$disconnect())test('Your unit test', async () => {// ...})
We're setting the database URL as file:./test-${process.env.VITEST_WORKER_ID}.db so that our tests can use one database per Vitest worker and run each test suite in parallel.
Context API
The context API lets you easily manipulate data in your system. We can use the Query API to ensure that we can do basic CRUD operations.
const person = await context.query.Person.createOne({data: { name: 'Alice', email: 'alice@example.com', password: 'super-secret' },query: 'id name email password { isSet }',})expect(person.name).toEqual('Alice')expect(person.email).toEqual('alice@example.com')expect(person.password.isSet).toEqual(true)
This API works well when we expect an operation to succeed. If we expect an operation to fail we can use the context.graphql.raw operation to check that both the data and errors returned by a query are what we expect.
// Create user without the required `name` fieldconst { data, errors } = await context.graphql.raw({query: `mutation {createPerson(data: { email: "alice@example.com", password: "super-secret" }) {id name email password { isSet }}}`,})expect(data.createPerson).toBe(null)expect(errors).toHaveLength(1)expect(errors[0].path).toEqual(['createPerson'])expect(errors[0].message).toEqual('You provided invalid data for this operation.\n - Person.name: Name must not be empty')
The context.withSession() function can be used to run queries as if you were logged in as a particular user. This can be useful for testing the behaviour of your access control rules. In the example below, the access control only allows users to update their own tasks.
// Create some usersconst [alice, bob] = await context.query.Person.createMany({data: [{ name: 'Alice', email: 'alice@example.com', password: 'super-secret' },{ name: 'Bob', email: 'bob@example.com', password: 'super-secret' },],})// Create a task assigned to Aliceconst task = await context.query.Task.createOne({data: {label: 'Experiment with Keystone',priority: 'high',isComplete: false,assignedTo: { connect: { id: alice.id } },},})// Check that we can't update the task when logged in as Bobconst { data, errors } = await context.withSession({ itemId: bob.id, data: {} }).graphql.raw({query: `mutation update($id: ID!) {updateTask(where: { id: $id }, data: { isComplete: true }) {id}}`,variables: { id: task.id },})expect(data!.updateTask).toBe(null)expect(errors).toHaveLength(1)expect(errors![0].path).toEqual(['updateTask'])expect(errors![0].message).toEqual(`Access denied: You cannot update the item '{"id":"${task.id}"}' - it may not exist`)
Related resources
Example Project: Testing →
Shows you how to write tests against the GraphQL API to your Keystone system. Builds on the Authentication example project.
Context API Reference →
The API for run-time functionality in your Keystone system. Use it to write business logic for access control, hooks, testing, GraphQL schema extensions, and more.
Query API Reference →
A programmatic API for running CRUD operations against your GraphQL API. For each list in your system you get an API at context.query.<listName>.