# The Ultimate Guide to Testing with Prisma: End-To-End Testing URL: https://www.prisma.io/blog/testing-series-4-OVXtDis201 Published: 2023-03-02 Updated: 2026-07-09 Authors: Sabin Adams Tags: education Series: The Ultimate Guide to Testing with Prisma Description: How to write end-to-end tests for a Prisma ORM app with Playwright, using page objects, fixtures, and Prisma Client for cleanup. Verified on Prisma 7.8. 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/faker` 10.5.0, Prisma ORM 7.8.0, `@prisma/client` 7.8.0, and `@prisma/adapter-pg` 7.8.0 on Node.js 22. The test database is a local [Prisma Postgres](https://www.prisma.io/docs/postgres) instance (`npx prisma dev`), replacing the Docker setup from the original 2023 article. Note that the `prisma-client` generator emits ES modules, so the test project must be ESM (see the setup note below). Two API details also changed: `faker.internet.userName()` is now `faker.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 [#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? [#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 [#technologies-you-will-use] * Prisma ORM 7 with the `prisma-client` generator and the `@prisma/adapter-pg` driver adapter * [Node.js](https://nodejs.org/en/) 20 or later * Prisma Postgres running locally * [Playwright](https://playwright.dev/) * [Faker](https://fakerjs.dev/) for random test data ### Assumed knowledge [#assumed-knowledge] * Basic knowledge of JavaScript or TypeScript * Basic knowledge of Prisma Client and its queries * The integration test setup from [part 3](https://www.prisma.io/blog/testing-series-3-aBUyF8nxAn) ## Set up the end-to-end project [#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: ```shell mkdir e2e && cd e2e npm init -y npm i -D @playwright/test @faker-js/faker npx playwright install ``` > **Note**: `npx playwright install` downloads 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`: ```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 [#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: ```ts // 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](https://www.prisma.io/blog/testing-series-3-aBUyF8nxAn): ```sh npx prisma dev -n testing ``` ## Give tests access to Prisma Client [#give-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: ```ts // 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 prisma ``` ## Pages [#pages] A [page object](https://playwright.dev/docs/pom) groups interactions with a page into reusable methods. Create `e2e/tests/pages/login.page.ts`: ```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] [Fixtures](https://playwright.dev/docs/test-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`: ```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({ 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_credentials` uses 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. * `account` uses 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 older `faker.internet.userName()`. > **Verified:** The `user_credentials` cleanup path was executed against the live database: a user created with Faker credentials was found via `prisma.user.findUnique`, then removed with `prisma.user.deleteMany`, and confirmed gone. Faker's `username()` and `password()` generators and Playwright's `page`/`expect` fixtures also ran successfully. The `LocalStorage` helper reads the browser context's local storage for the app origin: ```ts // 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 [#tests] With the fixtures in place, write the tests in `e2e/tests/auth.spec.ts`. Import `test` and `expect` from your fixture file: ```ts // e2e/tests/auth.spec.ts import { test, expect } from './fixtures/auth.fixture' test.describe('auth', () => { // tests go here }) ``` ### Redirect an unauthorized user to login [#redirect-an-unauthorized-user-to-login] This test does not need the `loginPage` fixture because it does not start on the login page: ```ts // 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 [#warn-a-user-with-incorrect-credentials] ```ts // 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 [#warn-a-user-who-submits-an-empty-form] ```ts // 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 [#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: ```ts // 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 [#redirect-to-the-home-page-after-signing-in] This test uses the `account` fixture, which creates a real user first, then logs in: ```ts // 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 `#signup` selectors and its messages). They run against the frontend and backend that Playwright's `webServer` starts. If you run these against your own UI, adjust the selectors and expected text to match your app. ## Why Playwright? [#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 [#frequently-asked-questions] ### Can I use Prisma Client inside Playwright tests to clean up data? Yes. Playwright runs in the Node runtime, so you can import Prisma Client into your fixtures and helpers. A common pattern is to generate unique credentials with Faker, let the test create a user through the UI, then delete that user with `prisma.user.deleteMany` after the test. This cleanup path was verified against a live local Prisma Postgres database on Prisma ORM 7.8. ### Why does importing the generated Prisma Client fail in a Playwright project? The Prisma 7 `prisma-client` generator emits ES modules, and the generated client uses `import.meta`. If the test project runs as CommonJS, you get `Cannot use 'import.meta' outside a module`. Add `"type": "module"` to the project's `package.json` so it runs as ESM. This was verified: the import failed under CommonJS and succeeded once the project was ESM. ### Has the Faker API for random usernames changed? Yes. `faker.internet.userName()` was renamed to `faker.internet.username()`. On `@faker-js/faker` 10, use `faker.internet.username()`. This was verified generating usernames on version 10.5.0. ## Summary & what's next [#summary--whats-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](https://www.prisma.io/blog/testing-series-5-xWogenROXm), you will run your unit, integration, and end-to-end tests automatically with GitHub Actions. You can also revisit [part 3: Integration Testing](https://www.prisma.io/blog/testing-series-3-aBUyF8nxAn). Looking ahead: [Prisma 8](https://www.prisma.io/docs/orm) is a TypeScript-native rewrite of Prisma ORM, built for AI coding agents, and it is now a [release candidate](https://www.prisma.io/docs/orm/release-status). Prisma 7 remains fully supported; its docs live at [prisma.io/docs/orm/v7](https://www.prisma.io/docs/orm/v7). To start with Prisma 8, run `npm create prisma@latest` or read the [Prisma 8 docs](https://www.prisma.io/docs/getting-started).