The Ultimate Guide to Testing with Prisma: Integration Testing
To integration test an app that uses Prisma ORM, you run the tests against a real database, reset that database between tests, and drive the API the way a client would. This article sets up an integration testing environment with Vitest, Supertest, and a local Prisma Postgres database, then writes integration tests for an Express API. This is part 3 of a five-part series on testing with Prisma ORM.
Updated (July 2026): This article was rewritten for Prisma ORM 7 and the current stack. All code was executed against Prisma ORM 7.8.0,
@prisma/client7.8.0,@prisma/adapter-pg7.8.0, Vitest 4.1.10,supertest7.2.2,bcrypt6.0.0, andjsonwebtoken9.0.3 on Node.js 22. The biggest change from the original 2023 article: the test database is now a local Prisma Postgres instance started withnpx prisma dev(powered by PGlite), which removes the Docker Compose setup, thewait-for-it.shscript, and the custom bash orchestration entirely. Vitest'sthreads: falseoption is also gone; the current option to disable parallelism isfileParallelism: false.
Introduction
So far in this series you mocked Prisma Client and used the mock to write unit tests against small, isolated units of code. In this article you will say goodbye to the mocked client and write integration tests against a real database.
What is integration testing?
Where unit tests confirm the smallest building blocks work in isolation, integration testing takes related components and confirms they work together.

The diagram illustrates a request that hits the database multiple times across different components. Because integration tests exercise these interactions, they run against a real database rather than a mock.
Technologies you will use
- Vitest 4
- Prisma ORM 7 with the
prisma-clientgenerator and the@prisma/adapter-pgdriver adapter - Node.js 20 or later
- Prisma Postgres running locally
- Supertest
Assumed knowledge
- Basic knowledge of JavaScript or TypeScript
- Basic knowledge of Prisma Client and its queries
- The project setup and unit tests from part 1 and part 2
Start a local test database
The original version of this article ran Postgres in a Docker container and orchestrated it with a shell script. On Prisma ORM 7 you can start a local Prisma Postgres database with a single command, no Docker required. It runs locally and is powered by PGlite.
In one terminal, start the database:
npx prisma dev -n testing✔ Your local Prisma Postgres server testing is now running 👍
🔌 To connect with Prisma ORM use the following connection strings:
DATABASE_URL="postgres://postgres:postgres@localhost:51254/template1?sslmode=disable&connection_limit=10&connect_timeout=0&max_idle_connection_lifetime=0&pool_timeout=0&socket_timeout=0"Leave that running. Put the DATABASE_URL it printed, along with an API_SECRET, in your .env file:
# .env
DATABASE_URL="postgres://postgres:postgres@localhost:51254/template1?sslmode=disable&..."
API_SECRET="supersecretstring"The API_SECRET is a secret key the authentication service uses to sign session tokens. In production, use a long random string.
Apply your schema to the database and generate the client:
npx prisma db push
npx prisma generate🚀 Your database is now in sync with your Prisma schema. Done in 71ms
✔ Generated Prisma Client (7.8.0) to ./src/generated/prisma in 43msNote:
prisma db pushis the right fit for a test database: it syncs the schema without creating migration files. Remember that on Prisma 7 you must runnpx prisma generateyourself after any schema change, becausedb pushandmigrate devno longer regenerate the client.
Add a Vitest configuration for integration tests
In part 2 you created vitest.config.unit.ts. Create a second config, vitest.config.integration.ts, for integration tests:
// vitest.config.integration.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['src/tests/**/*.test.ts'],
fileParallelism: false,
setupFiles: ['src/tests/helpers/setup.ts']
}
})Three things here:
test.includepoints to.tsfiles insrc/tests, so integration tests live in their own folder.fileParallelism: falseruns test files one at a time. This matters because the tests share one database; running them in parallel would cause them to see each other's data.setupFilesruns a setup file before your tests, used below to reset the database.
Updated (July 2026): The original used
threads: false. On Vitest 4 that option no longer exists; setting it has no effect (verified: the tests still ran in parallel-capable pools). The current way to force serial execution isfileParallelism: false.
Keep the unit config scoped so it does not pick up integration tests:
// vitest.config.unit.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['src/**/*.test.ts', '!src/tests']
}
})Reset the database between tests
Each integration test should start from a clean slate. Create a helper that instantiates a client for the tests. It uses the driver adapter and reads env, so it imports dotenv/config:
// src/tests/helpers/prisma.ts
import 'dotenv/config'
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '../../generated/prisma/client'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! })
const prisma = new PrismaClient({ adapter })
export default prismaAdd a reset function that clears every table inside a transaction:
// src/tests/helpers/reset-db.ts
import prisma from './prisma'
export default async () => {
await prisma.$transaction([
prisma.tag.deleteMany(),
prisma.quote.deleteMany(),
prisma.post.deleteMany(),
prisma.user.deleteMany()
])
}Then run that reset before every test using Vitest's beforeEach in the setup file:
// src/tests/helpers/setup.ts
import resetDb from './reset-db'
import { beforeEach } from 'vitest'
beforeEach(async () => {
await resetDb()
})Because you registered this file in setupFiles, every test across src/tests now starts with an empty database.
Configure npm scripts
Add scripts to package.json to run the integration tests:
{
"scripts": {
"test:int": "vitest -c ./vitest.config.integration.ts",
"test:int:ui": "vitest -c ./vitest.config.integration.ts --ui"
}
}With the database running, npm run test:int will connect to it directly. There is no container to spin up and no readiness script to wait on.
Write the integration tests
When deciding what to integration test, focus on the interactions between components. In this Express API, the important groupings are routes, controllers, and services. A request enters a route, the controller handles it, and services talk to the database.
You will test the /auth/signup and /auth/signin routes, driving them over HTTP so the tests mimic a real client.
Set up the test file
Create src/tests/auth.test.ts. Use Supertest to send requests to the Express app, and import the Prisma helper to inspect the database:
npm i -D supertest @types/supertest// src/tests/auth.test.ts
import 'dotenv/config'
import { beforeEach, describe, expect, it } from 'vitest'
import request from 'supertest'
import jwt from 'jsonwebtoken'
import bcrypt from 'bcrypt'
import app from '../lib/createServer'
import prisma from './helpers/prisma'
describe('/auth', () => {
describe('[POST] /auth/signup', () => {
// signup tests
})
})Tests for /auth/signup
The signup route creates a user from a username and password. Reading the controller and router, the behaviors worth testing are:
- It responds with
200and the user details. - It responds with a valid session token on success.
- It responds with
400if a user already exists with that username. - It responds with
400if the request body is invalid.
// src/tests/auth.test.ts (inside the [POST] /auth/signup describe)
it('should respond with a `200` status code and user details', async () => {
const { status, body } = await request(app).post('/auth/signup').send({
username: 'testusername',
password: 'testpassword'
})
const newUser = await prisma.user.findFirst()
expect(status).toBe(200)
expect(newUser).not.toBeNull()
expect(body.user).toStrictEqual({
username: 'testusername',
id: newUser?.id
})
})
it('should respond with a valid session token when successful', async () => {
const { body } = await request(app).post('/auth/signup').send({
username: 'testusername',
password: 'testpassword'
})
expect(body).toHaveProperty('token')
expect(jwt.verify(body.token, process.env.API_SECRET as string))
})
it('should respond with a `400` if a user already exists with that username', async () => {
await prisma.user.create({
data: { username: 'testusername', password: 'somepassword' }
})
const { status, body } = await request(app).post('/auth/signup').send({
username: 'testusername',
password: 'testpassword'
})
const count = await prisma.user.count()
expect(status).toBe(400)
expect(count).toBe(1)
expect(body).not.toHaveProperty('user')
})
it('should respond with a `400` if an invalid request body is provided', async () => {
const { body, status } = await request(app).post('/auth/signup').send({
email: 'test@prisma.io', // should be username
password: 'testpassword'
})
expect(status).toBe(400)
expect(body.message).toBe('Invalid or missing input provided for: username')
})Each test drives a real HTTP request through Supertest, then uses prisma to check what actually landed in the database. No modules are mocked; these run against the live database, and the setup file clears it between each test.
Tests for /auth/signin
The signin route validates an existing user. Its tests need a user in the database first, so use beforeEach inside this suite to create one. Match the same password hashing the auth service uses:
// src/tests/auth.test.ts (add a sibling describe)
describe('[POST] /auth/signin', () => {
beforeEach(async () => {
await prisma.user.create({
data: {
username: 'testusername',
password: bcrypt.hashSync('testpassword', 8)
}
})
})
it('should respond with a `200` when provided valid credentials', async () => {
const { status } = await request(app).post('/auth/signin').send({
username: 'testusername',
password: 'testpassword'
})
expect(status).toBe(200)
})
it('should respond with the user details when successful', async () => {
const { body } = await request(app).post('/auth/signin').send({
username: 'testusername',
password: 'testpassword'
})
const keys = Object.keys(body.user)
expect(keys.length).toBe(2)
expect(keys).toStrictEqual(['id', 'username'])
expect(body.user.username).toBe('testusername')
})
it('should respond with a valid session token when successful', async () => {
const { body } = await request(app).post('/auth/signin').send({
username: 'testusername',
password: 'testpassword'
})
expect(body).toHaveProperty('token')
expect(jwt.verify(body.token, process.env.API_SECRET as string))
})
it('should respond with a `400` when given invalid credentials', async () => {
const { body, status } = await request(app).post('/auth/signin').send({
username: 'testusername',
password: 'wrongpassword'
})
expect(status).toBe(400)
expect(body).not.toHaveProperty('token')
})
it('should respond with a `400` when the user cannot be found', async () => {
const { body, status } = await request(app).post('/auth/signin').send({
username: 'wrongusername',
password: 'testpassword'
})
expect(status).toBe(400)
expect(body).not.toHaveProperty('token')
})
it('should respond with a `400` when given an invalid request body', async () => {
const { body, status } = await request(app).post('/auth/signin').send({
email: 'test@prisma.io', // should be username
password: 'testpassword'
})
expect(status).toBe(400)
expect(body.message).toBe('Invalid or missing input provided for: username')
})
})Note: The password hashing in the test's
beforeEachmust match the hashing used in the auth service, or the signin tests will fail to authenticate the user.
Run npm run test:int. All ten tests pass against the live database:
✓ /auth > [POST] /auth/signup > should respond with a `200` status code and user details
✓ /auth > [POST] /auth/signup > should respond with a valid session token when successful
✓ /auth > [POST] /auth/signup > should respond with a `400` if a user already exists with that username
✓ /auth > [POST] /auth/signup > should respond with a `400` if an invalid request body is provided
✓ /auth > [POST] /auth/signin > should respond with a `200` when provided valid credentials
✓ /auth > [POST] /auth/signin > should respond with the user details when successful
✓ /auth > [POST] /auth/signin > should respond with a valid session token when successful
✓ /auth > [POST] /auth/signin > should respond with a `400` when given invalid credentials
✓ /auth > [POST] /auth/signin > should respond with a `400` when the user cannot be found
✓ /auth > [POST] /auth/signin > should respond with a `400` when given an invalid request body
Test Files 1 passed (1)
Tests 10 passed (10)Frequently asked questions
No. On Prisma ORM 7 you can start a local Prisma Postgres database with npx prisma dev, which runs locally (powered by PGlite) and prints a DATABASE_URL. That replaces the Docker Compose file, the readiness-wait script, and the custom bash orchestration the older approach used. Docker is still a valid option if you prefer it, but it is no longer required for local integration testing.
Two things. First, set fileParallelism: false in your Vitest config so test files run one at a time against the shared database. Second, register a setup file that clears every table in a beforeEach hook so each test starts from a clean slate. Note that threads: false from older Vitest versions no longer works; fileParallelism: false is the current option.
No. Integration tests exist to confirm your components work together against a real database, so you use a real Prisma Client connected to a test database. Mocking is for unit tests, where you isolate a single function. In these tests, Supertest drives real HTTP requests and Prisma Client reads the real database to verify the result.
Summary & what's next
During this article you:
- Learned what integration testing is
- Started a local Prisma Postgres test database with a single command
- Configured Vitest to run integration tests serially and reset the database between them
- Wrote integration tests for two API endpoints against a real database
In part 4: End-to-End Testing, you will test the application from a user's perspective with Playwright. You can also revisit part 2: Unit Testing.
Looking ahead: Prisma 8 is a TypeScript-native rewrite of Prisma ORM, built for AI coding agents, and it is now the current release. Prisma 7 remains fully supported; its docs live at prisma.io/docs/orm/v7. To start with Prisma 8, run npm create prisma@latest or read the Prisma 8 docs.
Keep reading
Agentic Engineering: How Prisma Builds with AI
In three months, Prisma fundamentally changed how it builds software by rethinking the relationship between engineers and AI agents. Here's what we learned building Prisma 8 with agentic engineering.

Build A Fullstack App with Remix, Prisma & MongoDB: Referential Integrity & Image Uploads
Learn how to build and deploy a fullstack application using Remix, Prisma, and MongoDB. In this article, we will be building the profile settings section of the website and enhancing the data model to provide better referential integrity.'

Build your next app with Prisma
Start free. Scale when you’re ready.