How to Reset Your Database Between Cypress Tests
Cypress tests that share database state are flaky and order-dependent. Use Seedmancer to restore a known PostgreSQL state before each spec or test for reliable E2E results.
Cypress is excellent at testing user interfaces. It simulates real browser interactions, makes assertions against what the user sees, and catches bugs that unit tests miss entirely. But Cypress tests live or die by their starting state, and that starting state lives in your database.
When tests share database state, they start depending on each other. Test A creates a user. Test B searches for that user. Test B passes when run after Test A and fails when run in isolation. Your test suite is no longer a reliable signal.
This guide covers how to reset a PostgreSQL database to a known state between Cypress tests, from a simple pre-run seed to per-spec seeding for fully isolated suites.
Why Cypress tests need a predictable database
Cypress tests are end-to-end: they go through the full stack, which means they read from and write to a real database. Every test that creates data leaves something behind. Every test that deletes data removes something the next test may have expected.
The most common symptoms of shared database state in Cypress:
- Tests pass when run alone (
cypress run --spec), fail when run as part of the full suite. - Tests pass locally, fail in CI (because CI starts with a different baseline).
- Tests that worked last week start failing after a teammate added a new test that creates conflicting data.
- Flaky tests that fail one in five runs for no reproducible reason.
All of these trace back to the same root cause: the database state at the start of each test is undefined.
The fix is to make the starting state explicit: seed a known scenario, run the tests, repeat.
Option 1: seed once before the full test run
The simplest approach is a shell script that seeds the database before running Cypress:
seedmancer seed myapp/baseline --yes && npx cypress run
Add it to package.json:
{
"scripts": {
"test:e2e": "seedmancer seed myapp/baseline --yes && cypress run"
}
}
This works well when your full test suite can share one starting state. The baseline scenario contains the reference data every test needs — default roles, product categories, a known set of users — and individual tests either use that data directly or create their own objects on top of it.
The downside is that tests still share state across the run. If a test modifies data, the tests that follow it run against a modified database.
Option 2: seed in before per spec file
Cypress spec files map naturally to application features. Each spec usually has a coherent starting state: the billing spec needs a pro workspace, the auth spec needs accounts in different states, the search spec needs a populated product catalog.
Use Cypress's before hook to seed the right scenario before each spec:
// cypress/e2e/billing.cy.js
before(() => {
cy.exec("seedmancer seed billing/pro --yes")
})
describe("Billing page", () => {
it("shows active subscription details", () => {
cy.visit("/billing")
cy.contains("Pro Plan").should("be.visible")
})
it("allows downloading invoices", () => {
cy.visit("/billing/invoices")
cy.get('[data-testid="invoice-row"]').should("have.length.greaterThan", 0)
})
})
cy.exec() runs a shell command from within a Cypress test. before runs once before all it blocks in the current describe. The scenario is seeded once per spec, not once per test.
This is the right balance for most teams: each spec starts from the state it needs, and tests within the spec share that state without polluting other specs.
Option 3: seed in beforeEach for full isolation
If tests within a spec write to the database in ways that affect each other, seed before every individual test:
// cypress/e2e/user-management.cy.js
beforeEach(() => {
cy.exec("seedmancer seed myapp/baseline --yes")
})
it("creates a new user", () => {
cy.visit("/admin/users")
cy.get('[data-testid="add-user"]').click()
cy.get('[name="email"]').type("newuser@example.com")
cy.get('[type="submit"]').click()
cy.contains("User created").should("be.visible")
})
it("rejects duplicate email", () => {
cy.visit("/admin/users")
cy.get('[data-testid="add-user"]').click()
cy.get('[name="email"]').type("existing@example.com")
cy.get('[type="submit"]').click()
cy.contains("Email already in use").should("be.visible")
})
The second test relies on existing@example.com being present in the database. That row is part of the myapp/baseline scenario — it is seeded before every test, so it is always there regardless of what the previous test did.
This is the strongest form of isolation. The trade-off is speed: a seed runs before every it block. Seedmancer restores CSVs in under a second for most datasets, so the overhead is usually a few hundred milliseconds per test.
Using a Cypress task for cleaner syntax
If you prefer not to use cy.exec() directly, register a Cypress task in cypress.config.js:
const { defineConfig } = require("cypress")
const { execSync } = require("child_process")
module.exports = defineConfig({
e2e: {
setupNodeEvents(on) {
on("task", {
seedDatabase(scenario) {
execSync(`seedmancer seed ${scenario} --yes`, { stdio: "inherit" })
return null
}
})
}
}
})
Then in your specs:
beforeEach(() => {
cy.task("seedDatabase", "myapp/baseline")
})
The task approach is cleaner when you need to seed different scenarios from the same spec, or when you want to pass the scenario name as a variable.
Running in CI
In GitHub Actions:
- name: Install Seedmancer
run: go install github.com/KazanKK/seedmancer@latest
- name: Run Cypress tests
run: npm run test:e2e
env:
SEEDMANCER_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
If seeding is done inside Cypress (via cy.exec or tasks), the SEEDMANCER_DATABASE_URL environment variable is read automatically. If seeding is done in a separate shell step before cypress run, the same variable applies.
The scenario lives in .seedmancer/ in your repository — no cloud account needed, no data downloaded at CI time.
Choosing the right approach
| Approach | Best for |
|---|---|
| Shell script before cypress run | All specs share a single starting state |
| before per spec + scenario seed | Each spec needs a different application state |
| beforeEach per test | Tests write to the database and cannot share state |
Start with option 1 if your test suite is small. Move to option 2 as the suite grows and different specs need different states. Reserve option 3 for specs where tests genuinely cannot share state.
The goal is not maximum isolation for its own sake — it is reliable, fast tests. Seeding once per spec is usually the right balance.
See the CLI documentation for the full command reference and environment variable setup.