The Ultimate Guide to Testing with Prisma: End-To-End Testing
To end-to-end test an app that uses Prisma ORM, you drive the real UI with a browser automation tool, and you use Prisma Client inside your test fixtures to seed and clean up data. This article writes end-to-end tests for the authentication flow with Playwright, using page objects and fixtures, with Prisma Client handling cleanup. This is part 4 of a five-part series on testing with Prisma ORM.
Updated (July 2026): This article was rewritten for Prisma ORM 7 and the current tooling. The Playwright APIs, page-object and fixture patterns, the Prisma Client cleanup pattern inside a fixture, and Faker's data generators were executed against Playwright 1.61.1,
@faker-js/faker10.5.0, Prisma ORM 7.8.0,@prisma/client7.8.0, and@prisma/adapter-pg7.8.0 on Node.js 22. The test database is a local Prisma Postgres instance (npx prisma dev), replacing the Docker setup from the original 2023 article. Note that theprisma-clientgenerator emits ES modules, so the test project must be ESM (see the setup note below). Two API details also changed:faker.internet.userName()is nowfaker.internet.username(). The browser-driven steps target the sample app's login UI and require the frontend and backend running; the Prisma-and-Playwright plumbing shown here was executed directly.
Introduction
At this point in the series you have written unit and integration tests for a standalone Express API. In this article you add a React frontend that consumes that API and write end-to-end tests that confirm the interactions a user makes work correctly.
What is end-to-end testing?
End-to-end testing emulates user interactions within an application to confirm they work. Where earlier tests verified individual building blocks, end-to-end tests confirm the whole stack behaves as a user expects. For example:
- If a user visits the home page while signed out, are they redirected to login?
- If a user submits an empty login form, are they warned?
- If a user creates an account, are they redirected to the home page?
These tests act as if the test runner were a real user, exercising the frontend and backend together.
Technologies you will use
- Prisma ORM 7 with the
prisma-clientgenerator and the@prisma/adapter-pgdriver adapter - Node.js 20 or later
- Prisma Postgres running locally
- Playwright
- Faker for random test data
Assumed knowledge
- Basic knowledge of JavaScript or TypeScript
- Basic knowledge of Prisma Client and its queries
- The integration test setup from part 3
Set up the end-to-end project
Your end-to-end tests belong in their own project because they are neither frontend nor backend; they exercise both. Create an e2e folder and install Playwright and Faker:
mkdir e2e && cd e2e
npm init -y
npm i -D @playwright/test @faker-js/faker
npx playwright installNote:
npx playwright installdownloads the browser binaries and may take a while.
The Prisma 7 prisma-client generator emits ES modules, and the generated client uses import.meta. If your test project runs as CommonJS, importing the client fails with Cannot use 'import.meta' outside a module. Mark the e2e project as ESM in its package.json:
{
"name": "e2e",
"type": "module",
"private": true
}Verified: Without
"type": "module", Playwright failed to load the generated Prisma Client. With ESM set on the project, the client imported and ran correctly.
Configure Playwright
Create e2e/playwright.config.ts. Prisma 7 does not auto-load .env, so load the project's env file, and configure Playwright to start the backend and frontend before running tests:
// e2e/playwright.config.ts
import dotenv from 'dotenv'
import { defineConfig, devices } from '@playwright/test'
dotenv.config({ path: '../.env' })
export default defineConfig({
testDir: './tests',
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: [
{ command: 'npm run --prefix ../backend dev', port: 3000, reuseExistingServer: true },
{ command: 'npm run --prefix ../frontend dev', port: 5173, reuseExistingServer: true }
]
})The webServer array starts the backend on port 3000 and the frontend on port 5173, waiting for each port before running tests. reuseExistingServer: true reuses a server you already have running.
Start the test database in a separate terminal, as in part 3:
npx prisma dev -n testingGive tests access to Prisma Client
Playwright runs in the Node runtime, so you can use Prisma Client directly in tests and fixtures to seed and clean up data.
Create e2e/tests/helpers/prisma.ts, using the driver adapter and the generated client path:
// e2e/tests/helpers/prisma.ts
import 'dotenv/config'
import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '../../../src/generated/prisma/client'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! })
const prisma = new PrismaClient({ adapter })
export default prismaPages
A page object groups interactions with a page into reusable methods. Create e2e/tests/pages/login.page.ts:
// e2e/tests/pages/login.page.ts
import type { Page } from '@playwright/test'
export class LoginPage {
readonly page: Page
constructor(page: Page) {
this.page = page
}
async goto() {
await this.page.goto('http://localhost:5173/login')
await this.page.waitForURL('http://localhost:5173/login')
}
async populateForm(username: string, password: string) {
await this.page.fill('#username', username)
await this.page.fill('#password', password)
}
}Fixtures
Fixtures let you provide reusable objects to your tests. You will build fixtures that expose the login page, generate unique credentials, create an account, and read local storage.
Create e2e/tests/fixtures/auth.fixture.ts:
// e2e/tests/fixtures/auth.fixture.ts
import { test as base } from '@playwright/test'
import { LoginPage } from '../pages/login.page'
import { LocalStorage } from '../helpers/LocalStorage'
import prisma from '../helpers/prisma'
import { faker } from '@faker-js/faker'
type UserDetails = {
username: string
password: string
}
type AuthFixtures = {
loginPage: LoginPage
user_credentials: UserDetails
account: UserDetails
storage: LocalStorage
}
export const test = base.extend<AuthFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await use(loginPage)
},
user_credentials: async ({}, use) => {
const username = faker.internet.username()
const password = faker.internet.password()
await use({ username, password })
// Clean up any user created with these credentials
await prisma.user.deleteMany({ where: { username } })
},
account: async ({ browser, user_credentials }, use) => {
const page = await browser.newPage()
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.populateForm(
user_credentials.username,
user_credentials.password
)
await page.click('#signup')
await page.waitForLoadState('networkidle')
await page.close()
await use(user_credentials)
},
storage: async ({ page }, use) => {
const storage = new LocalStorage(page.context())
await use(storage)
}
})
export { expect } from '@playwright/test'A few notes:
user_credentialsuses Faker to generate a unique username and password, provides them to the test, then deletes the matching user with Prisma Client after the test. This is the cleanup pattern that keeps the test database clean.accountuses those credentials to create a real account through the sign-up form, so tests that need an existing user get one.faker.internet.username()replaced the olderfaker.internet.userName().
Verified: The
user_credentialscleanup path was executed against the live database: a user created with Faker credentials was found viaprisma.user.findUnique, then removed withprisma.user.deleteMany, and confirmed gone. Faker'susername()andpassword()generators and Playwright'spage/expectfixtures also ran successfully.
The LocalStorage helper reads the browser context's local storage for the app origin:
// e2e/tests/helpers/LocalStorage.ts
import type { BrowserContext } from '@playwright/test'
export class LocalStorage {
private context: BrowserContext
constructor(context: BrowserContext) {
this.context = context
}
get localStorage() {
return this.context.storageState().then((storage) => {
const origin = storage.origins.find(
({ origin }) => origin === 'http://localhost:5173'
)
if (origin) {
return origin.localStorage.reduce(
(acc, curr) => ({ ...acc, [curr.name]: curr.value }),
{}
)
}
return {}
})
}
}Tests
With the fixtures in place, write the tests in e2e/tests/auth.spec.ts. Import test and expect from your fixture file:
// e2e/tests/auth.spec.ts
import { test, expect } from './fixtures/auth.fixture'
test.describe('auth', () => {
// tests go here
})Redirect an unauthorized user to login
This test does not need the loginPage fixture because it does not start on the login page:
// e2e/tests/auth.spec.ts
test('should redirect unauthorized user to the login page', async ({ page }) => {
await page.goto('http://localhost:5173/')
await expect(page).toHaveURL('http://localhost:5173/login')
})Warn a user with incorrect credentials
// e2e/tests/auth.spec.ts
test('should warn you if your login is incorrect', async ({ page, loginPage }) => {
await loginPage.populateForm('incorrect', 'password')
await page.click('#login')
await page.waitForLoadState('networkidle')
await expect(page.getByText('Account not found.')).toBeVisible()
})Warn a user who submits an empty form
// e2e/tests/auth.spec.ts
test('should warn you if your form is empty', async ({ page, loginPage }) => {
await loginPage.page.click('#login')
await page.waitForLoadState('networkidle')
await expect(
page.getByText('Please enter a username and password')
).toBeVisible()
})Redirect to the home page after creating an account
This test uses user_credentials for unique data and storage to confirm the user landed in local storage:
// e2e/tests/auth.spec.ts
test('should redirect to the home page when a new account is created', async ({
user_credentials,
loginPage,
storage,
page
}) => {
await loginPage.populateForm(
user_credentials.username,
user_credentials.password
)
await page.click('#signup')
await page.waitForLoadState('networkidle')
const localStorage = await storage.localStorage
expect(localStorage).toHaveProperty('quoots-user')
await expect(page).toHaveURL('http://localhost:5173')
})Redirect to the home page after signing in
This test uses the account fixture, which creates a real user first, then logs in:
// e2e/tests/auth.spec.ts
test('should redirect to the home page after signing in', async ({
account,
loginPage,
storage,
page
}) => {
await loginPage.populateForm(account.username, account.password)
await page.click('#login')
await page.waitForLoadState('networkidle')
const localStorage = await storage.localStorage
expect(localStorage).toHaveProperty('quoots-user')
await expect(page).toHaveURL('http://localhost:5173')
})Run the tests with npx playwright test. Because the account fixture requires user_credentials, the generated user is deleted after each test that uses it, keeping the database clean.
Note: The browser-driven assertions above target the sample application's login UI (its
#username,#login, and#signupselectors and its messages). They run against the frontend and backend that Playwright'swebServerstarts. If you run these against your own UI, adjust the selectors and expected text to match your app.
Why Playwright?
Playwright fits this use case for a few reasons: it is straightforward to configure, its API is extensible, and its fixture system is flexible. Because Playwright runs in the Node runtime, you can import and use Prisma Client directly inside fixtures to seed and clean up test data, as shown above. That combination of an extensible fixture system and direct database access is what makes it a good fit here.
Frequently asked questions
Summary & what's next
Throughout this article you:
- Learned what end-to-end testing is
- Set up a dedicated end-to-end project and configured it as ESM for the Prisma 7 client
- Built page objects and fixtures, using Prisma Client for cleanup
- Wrote tests for the authentication workflows
In part 5: CI Pipelines, you will run your unit, integration, and end-to-end tests automatically with GitHub Actions. You can also revisit part 3: Integration Testing.
Looking ahead: Prisma Next is a TypeScript-native rewrite of Prisma ORM, built for AI coding agents and currently in early access. It becomes Prisma 8 at general availability; until then, Prisma 7 stays the production choice. To try it, run npm create prisma@next or read the early access docs.
Build your next app with Prisma
Start free. Scale when you’re ready.
