Q38 of 42 · Playwright
How do you handle deterministic test data when tests run in parallel?
Short answer
Short answer: Each test gets isolated data, namespaced by worker index or test ID. Two strategies: per-test seeding (worker fixture creates rows with unique prefixes), or transactional rollback (each test runs in a DB transaction, rolled back on teardown). Avoid shared mutable fixtures.
Detail
Parallel tests share the test environment; without discipline, one test's writes pollute another's reads. Three patterns scale:
1. Namespace per worker. Worker fixture seeds data with a worker-specific prefix:
export const test = base.extend<{}, { workerSeed: { prefix: string } }>({
workerSeed: [async ({}, use, workerInfo) => {
const prefix = `w${workerInfo.workerIndex}`;
await db.users.insert({ email: `${prefix}-admin@x.com` });
await use({ prefix });
await db.users.deleteWhere({ email: { startsWith: prefix } });
}, { scope: 'worker' }],
});
test('something', async ({ page, workerSeed }) => {
const adminEmail = `${workerSeed.prefix}-admin@x.com`;
// ...
});
Each worker has its own data; no collision. Scales horizontally with workers.
2. Per-test unique IDs. Inside each test, generate a unique ID and use it in created data:
test('creates an order', async ({ page, request }) => {
const id = `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
await request.post('/api/orders', { data: { externalId: id, ... } });
// Assert / cleanup using the unique id
});
Simple, but cleanup is the test's responsibility.
3. Transactional rollback (the cleanest, when supported). Each test runs inside a database transaction; teardown rolls back. The DB state is bit-for-bit identical at the end of each test.
test.beforeEach(async () => {
await db.exec('BEGIN');
});
test.afterEach(async () => {
await db.exec('ROLLBACK');
});
This requires the app to use a single DB connection per test (often via a dedicated test environment / connection-bound session), and your DB to support nested transactions or savepoints. Works beautifully for backend integration tests; harder for full E2E because the app server likely uses a connection pool.
4. Snapshot + restore for heavyweight test data. Take a DB dump at suite start; restore between tests or workers. Slower per-test but maximally isolated.
Anti-patterns:
- Shared mutable fixtures with no isolation — race conditions, ordering coupling.
- Deletion at suite end only — leaks if a test crashes; later runs pick up stale data.
- Random data with no cleanup — DB grows monotonically.
Senior signal: matching strategy to the test layer (rollback for backend integration, namespace + cleanup for E2E), and explicit cleanup discipline.
// WHAT INTERVIEWERS LOOK FOR
// COMMON PITFALL
// Related questions