← Back to Blog

The Ultimate Guide to Testing with Prisma: End-To-End Testing

Sabin Adams
Sabin Adams
March 2, 2023
Updated July 9, 2026

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 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

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-client generator and the @prisma/adapter-pg driver 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 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:

{
  "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 testing

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:

// 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

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_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:

// 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 #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?

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.

Try Prisma
Share this article