How to Seed a PostgreSQL Database Before Playwright Tests
Stop Playwright tests from interfering with each other. Use globalSetup, beforeAll, or beforeEach to restore a known PostgreSQL state before every test run.
Playwright is designed to test your application the way a real user would. It opens a browser, clicks buttons, fills in forms, and verifies the result. That realism is exactly what makes it valuable — and exactly what makes shared database state so dangerous.
When two Playwright tests touch the same user record, the same order, or the same session, they start interfering with each other in ways that are hard to reproduce and harder to debug. The fix is straightforward: seed the database to a known state before each test run, so every test starts from the same predictable baseline.
This guide covers three ways to do that with Seedmancer and PostgreSQL.
The problem with Playwright and shared state
Playwright tests typically run against a real application connected to a real database. That is what makes them useful for catching integration bugs that unit tests miss.
The downside is that every test that writes to the database leaves something behind. A user created in one test may conflict with a uniqueness constraint in the next. A status updated mid-test may be in the wrong state for a subsequent assertion. Tests that pass in isolation start failing when run together.
The standard advice is to clean up after each test, but cleanup logic is fragile. If a test fails halfway through, the cleanup step may never run. If a new table is added and nobody updates the cleanup, test isolation quietly breaks.
Seeding to a known state before tests run is more reliable than trying to clean up after them.
Option 1: seed before the full test run with globalSetup
The simplest integration point for Playwright is globalSetup. Playwright runs this file once before any test begins. It is the right place to reset the database to a baseline.
Create a playwright.config.ts that points at a global setup file:
import { defineConfig } from "@playwright/test"
export default defineConfig({
globalSetup: "./global-setup.ts",
use: {
baseURL: "http://localhost:3000"
}
})
In global-setup.ts, run the seed command:
import { execSync } from "child_process"
export default async function globalSetup() {
execSync("seedmancer seed myapp/baseline --yes", { stdio: "inherit" })
}
--yes skips the interactive confirmation so the setup does not hang waiting for input.
Now every npx playwright test run starts from the same database state. The baseline revision is pinned to your schema fingerprint, so if someone changes the schema and forgets to update the scenario, Seedmancer blocks the seed and tells you which scenario needs refreshing rather than silently restoring bad data.
Option 2: seed a specific scenario per test file
Not every test needs the same starting state. A billing flow test needs a workspace with a pro subscription. An onboarding test needs a brand-new user with no data. A permission test needs a locked account.
Playwright's test.beforeAll hook lets you seed a targeted scenario before the tests in a single file:
import { test, expect, chromium } from "@playwright/test"
import { execSync } from "child_process"
test.beforeAll(async () => {
execSync("seedmancer seed billing/pro --yes", { stdio: "inherit" })
})
test("shows upgrade prompt when trial expires", async ({ page }) => {
await page.goto("/billing")
await expect(page.getByText("Your trial has expired")).toBeVisible()
})
test("allows downloading invoices on pro plan", async ({ page }) => {
await page.goto("/billing/invoices")
await expect(page.getByRole("link", { name: /Download/ })).toBeVisible()
})
Each test file seeds the scenario it cares about. Tests within the file share that state, which is fine as long as they do not write to the database in ways that affect each other. If they do, move to per-test seeding (see option 3).
This approach keeps your baseline scenario lean — it only needs the reference data that all tests share — and puts scenario-specific data close to the tests that use it.
Option 3: seed before each individual test
For tests that mutate the database and cannot safely share state, seed before each test:
import { test, expect } from "@playwright/test"
import { execSync } from "child_process"
test.beforeEach(async () => {
execSync("seedmancer seed myapp/baseline --yes", { stdio: "inherit" })
})
test("creates a new account", async ({ page }) => {
await page.goto("/signup")
await page.fill('[name="email"]', "newuser@example.com")
await page.fill('[name="password"]', "securepass")
await page.click('[type="submit"]')
await expect(page.getByText("Welcome")).toBeVisible()
})
test("rejects duplicate email", async ({ page }) => {
// the baseline already has a user with this email
await page.goto("/signup")
await page.fill('[name="email"]', "existing@example.com")
await page.fill('[name="password"]', "securepass")
await page.click('[type="submit"]')
await expect(page.getByText("Email already in use")).toBeVisible()
})
This gives the strongest isolation but is slower because a seed runs before every test. Seedmancer restores CSVs in under a second for most datasets, so the overhead is usually acceptable. If you have hundreds of tests, prefer option 2 and group tests that share state into the same file.
Wiring it into your npm scripts
Add the seed step to your package.json so local runs and CI use the same command:
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:billing": "seedmancer seed billing/pro --yes && playwright test tests/billing"
}
}
Or, if you use globalSetup, the seed is already part of playwright test and you do not need a wrapper.
Running in CI
In GitHub Actions:
- name: Install Seedmancer
run: go install github.com/KazanKK/seedmancer@latest
- name: Seed database
run: seedmancer seed myapp/baseline --yes
env:
SEEDMANCER_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
- name: Run Playwright tests
run: npx playwright test
If you use globalSetup, the seed runs automatically as part of the Playwright invocation and you only need to expose SEEDMANCER_DATABASE_URL.
The scenario lives in .seedmancer/ in your repository, so no cloud account is needed in CI. Everyone on the team — and every CI runner — uses the exact same revision.
Keeping scenarios up to date
When your schema changes, seedmancer check myapp/baseline tells you if the stored revision is still compatible. If not, refresh it:
seedmancer refresh myapp/baseline
The refresh command (Pro plan) uses the Seedmancer cloud AI. Or let an MCP agent in Cursor or Claude handle the update locally: it rewrites the saved generation SQL for the new schema and calls generate_dataset_local.
The refreshed state becomes a new revision (r002, r003, …). Old revisions are preserved. Your playwright.config.ts and test files do not need to change — latest always points at the most recent compatible revision.
Summary
| Approach | When to use it |
|---|---|
| globalSetup + baseline seed | All tests share the same starting state |
| beforeAll per file + scenario seed | Different test files need different states |
| beforeEach + scenario seed | Tests mutate the database and cannot share state |
Seeding before tests, rather than cleaning up after them, is more reliable and easier to maintain. A one-second restore to a known scenario is simpler than tracking every write a test makes and reversing it.
See the CLI documentation for the full flag reference and environment setup.