Q38 of 42 · Playwright

How do you handle deterministic test data when tests run in parallel?

PlaywrightSeniorplaywrighttest-dataparallelisolationsenior

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

Naming all three strategies (worker namespace, per-test unique IDs, transactional rollback) and matching them to test layers (E2E vs integration).

// COMMON PITFALL

Sharing mutable fixtures across parallel tests — works locally with 1 worker, fails in CI with 4.