Skip to Page NavigationSkip to Page NavigationSkip to Content

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-ui
prisma 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` field
const { 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 users
const [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 Alice
const 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 Bob
const { 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`
)