Seeding test data is the difference between tests that explain exactly what they're testing and tests that hide their preconditions in a maze of shared fixtures. In 2026, the standard approach is factory functions backed by Faker.js — each test builds exactly the data it needs, nothing more, and tears it down cleanly. This guide covers factories, Prisma seeds, and isolation strategies.
What changed in 2026
- Faker.js v9 is the maintained successor to the deprecated
@faker-js/faker; API is stable, locale support expanded.
fishery and @anatine/zod-mock are popular factory libraries in the TS ecosystem alongside plain factory functions.
- Vitest 2.x improved transaction-based test isolation: wrapping each test in a DB transaction and rolling back is now idiomatic.
- Snapshot-based seeding (Neon branching, PlanetScale branching) lets you clone production-like data cheaply for integration tests.
Factory functions with Faker.js
import { faker } from '@faker-js/faker';
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'member';
createdAt: Date;
}
export function makeUser(overrides: Partial<User> = {}): User {
return {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
role: 'member',
createdAt: faker.date.recent({ days: 30 }),
...overrides,
};
}
// In a test:
const admin = makeUser({ role: 'admin' });
const user = makeUser({ email: 'test@example.com' });
Overrides let you control exactly the properties relevant to the test; everything else is realistic noise.
Deterministic seeds
import { faker } from '@faker-js/faker';
beforeEach(() => {
faker.seed(42); // same sequence every run
});
test('generates consistent data', () => {
const user = makeUser();
expect(user.name).toBe('Kellie Runolfsson'); // always the same with seed 42
});
Use a fixed seed in CI. For local dev, let Faker randomize (omit faker.seed()) to catch edge cases over time.
Inserting factories into the database
import { db } from '@/lib/db'; // Prisma client
export async function createUser(overrides: Partial<UserCreateInput> = {}) {
return db.user.create({
data: {
name: faker.person.fullName(),
email: faker.internet.email(),
role: 'member',
...overrides,
},
});
}
export async function createPost(authorId: string, overrides = {}) {
return db.post.create({
data: {
title: faker.lorem.sentence(),
body: faker.lorem.paragraphs(3),
authorId,
...overrides,
},
});
}
Compose for relational data:
const author = await createUser({ role: 'admin' });
const post = await createPost(author.id, { title: 'My specific title' });
Transaction-based isolation (Vitest + Prisma)
import { beforeEach, afterEach } from 'vitest';
import { db } from '@/lib/db';
let tx: PrismaTransaction;
beforeEach(async () => {
tx = await db.$transaction(async (t) => {
// run all test DB ops through `tx`, then roll back
return t;
});
});
afterEach(async () => {
// Rollback by using $executeRaw or a test-specific schema
await db.$executeRaw`ROLLBACK`;
});
A simpler alternative for Postgres: each test suite creates a schema named after itself, seeds it, and drops it on teardown. No shared mutable state.
Prisma seed script for dev databases
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import { faker } from '@faker-js/faker';
const prisma = new PrismaClient();
async function main() {
faker.seed(1);
const admin = await prisma.user.upsert({
where: { email: 'admin@example.com' },
update: {},
create: { email: 'admin@example.com', name: 'Admin User', role: 'admin' },
});
for (let i = 0; i < 20; i++) {
await prisma.post.create({
data: {
title: faker.lorem.sentence(),
body: faker.lorem.paragraphs(2),
authorId: admin.id,
},
});
}
console.log('Seed complete');
}
main().finally(() => prisma.$disconnect());
In package.json:
"prisma": { "seed": "tsx prisma/seed.ts" }
Run with npx prisma db seed after migrations.
Seeding strategies compared
| Strategy |
Isolation |
Reproducibility |
Speed |
Use case |
| Factory functions + rollback |
Per-test |
With faker.seed() |
Fast |
Unit + integration tests |
| Truncate + re-seed |
Per-suite |
Fixed seed |
Medium |
Integration tests |
| Separate DB per test run |
Full |
Full |
Slow (DB create) |
Heavy integration / E2E |
| DB branching (Neon/PlanetScale) |
Full |
Prod-like data |
Fast once cloned |
E2E against real data shapes |
How to start
- Install
@faker-js/faker and write factory functions for your core models.
- Add a
prisma/seed.ts (or equivalent) that seeds dev DB in under 10 seconds.
- In tests, use factory functions; never rely on data seeded by another test.
- Set
faker.seed(42) in CI test setup for reproducibility.
- Use transactions or per-test schemas to isolate DB state.
Common mistakes
Shared mutable fixtures between tests. Test A mutates a user; test B reads it and fails unpredictably. Each test must own its data.
Seeding too much data. A seed script that inserts 100k rows runs for minutes. Seed only enough to make the UI usable (~20–50 representative records).
Hardcoded IDs in factories. Hardcoded UUIDs in test factories collide across parallel test runs. Always generate fresh IDs.
Not cleaning up. Tests that insert data without cleanup leave garbage that makes CI slow and debugging confusing. Roll back or truncate.
What to skip
- Large SQL dump fixtures — they conflict on merge, go stale quickly, and are painful to update.
- Using production data in tests — it leaks PII and makes test assertions fragile against real-world variation.
- Global
beforeAll seeds — makes individual tests unrunnable in isolation; prefer beforeEach or test-local setup.
FAQ
Should I use factories or fixtures?
Factories for most tests — they scale with your schema. Static fixtures only for a handful of truly fixed reference data (e.g., country codes, product categories).
How do I seed relational data with foreign keys?
Insert in dependency order: parent before child. Factory functions that accept an optional parent ID and create one if not provided handle this cleanly.
Can I seed data for E2E tests (Playwright, Cypress)?
Yes — call your seed factories via an API endpoint (gated to test environment only) or a custom globalSetup script that hits the DB directly.
How do I keep seed scripts fast in CI?
Use upsert so re-runs are idempotent; use createMany for bulk inserts; set faker.seed() so there is no need to randomize on every run.
Where to go next