Definition
- Static fixtures – JSON or CSV files with stable, versioned reference data. Simple but shared and can go stale.
- DB seeding – Insert known rows directly. Fast for complex relational preconditions but must avoid parallel collisions.
- API-generated data – Create preconditions through the app's own endpoints. Best: goes through real validation, returns real IDs.
- Factory methods – Build valid objects programmatically with defaults and selective overrides.
- Synthetic data – Generate realistic fake values for uniqueness and coverage.
- Parallel isolation – Per-worker or per-test data with unique keys, independent setup and teardown.
When many testers at the National Bank of India run checks at the same time, each needs their own sample customers and accounts, or they will step on each other. Good test-data management gives every tester their own set of practice accounts with unique numbers, and each tester tidies up afterward so the next run starts clean.
Understanding the Concepts
What are the sources of test data?
Choose the source by the situation. Each has strengths and trade-offs.
// 1. Static fixture (JSON)
import testCases from '../data/loan-cases.json';
// Good: versioned, readable. Risk: shared, can go stale.
// 2. API-generated data (best for most cases)
const response = await request.post('/api/test/accounts', {
data: { balance: 50000, currency: 'INR' },
});
const account = await response.json();
// Good: real validation, real IDs, consistent with business rules.
// 3. Factory method
function createAccount(overrides = {}) {
return {
id: `ACC-${Date.now()}-${Math.random().toString(36).slice(2)}`,
balance: 50000,
currency: 'INR',
status: 'ACTIVE',
...overrides,
};
}
// Good: sensible defaults, override only what you care about.
// 4. Synthetic data
import { faker } from '@faker-js/faker';
const customer = {
name: faker.person.fullName(),
email: faker.internet.email(),
phone: faker.phone.number(),
};
// Good: realistic, unique, avoids hard-coded values.
How does parallel interference happen?
When parallel tests share mutable data, one test corrupts another's expectations.
Worker 1: Transfer 5000 from ACC-001
Worker 2: Check balance of ACC-001
Worker 1 debits 5000 -> balance is now 45000
Worker 2 expects 50000 -> FAILS (flaky!)
The tests are correct individually.
The interference is the shared ACC-001.
How do you isolate data per worker?
Fold the worker index into identifiers so workers never share mutable rows.
test('Transfer with isolated data', async ({ page }, testInfo) => {
const w = testInfo.workerIndex;
const sender = `ACC-W${w}-SENDER-${Date.now()}`;
const receiver = `ACC-W${w}-RECEIVER-${Date.now()}`;
// Create unique accounts for THIS worker
await apiHelper.createAccount(sender, 100000);
await apiHelper.createAccount(receiver, 0);
// Transfer between worker-specific accounts
await page.goto(`/transfer?from=${sender}&to=${receiver}`);
// No other worker touches these accounts
});
How do you build factory methods?
Factories provide sensible defaults and let tests override only what matters.
// AccountFactory.java
package com.bank.testutil;
import java.math.BigDecimal;
import java.util.UUID;
public class AccountFactory {
public static Account create() {
return create(new BigDecimal("50000.00"));
}
public static Account create(BigDecimal balance) {
return new Account(
"ACC-" + UUID.randomUUID().toString().substring(0, 8),
balance,
"INR",
AccountStatus.ACTIVE
);
}
public static Account createDormant() {
Account acc = create();
acc.setStatus(AccountStatus.DORMANT);
acc.setLastActivityDate(
java.time.LocalDate.now().minusDays(200)
);
return acc;
}
}
How do you handle setup and teardown?
Each test creates its own data and cleans up independently.
// Fixture with automatic cleanup
export const test = base.extend<{ api: ApiHelper }>({
api: async ({ request }, use) => {
const api = new ApiHelper(request);
await use(api);
await api.cleanup(); // Deletes everything this test created
},
});
// ApiHelper tracks created resources
export class ApiHelper {
private created: string[] = [];
async createAccount(id: string, balance: number) {
await this.request.post('/api/test/accounts', {
data: { id, balance },
});
this.created.push(id);
}
async cleanup() {
for (const id of this.created) {
await this.request.delete(`/api/test/accounts/${id}`);
}
}
}
What are the rules for parallel-safe data?
| Rule | How |
|---|---|
| Never hard-code a shared mutable record | Use factories with unique IDs |
| Make keys unique by construction | UUID, timestamp, or worker index |
| Each test creates its own data | API helpers or factories |
| Each test cleans up its own data | Teardown in fixtures |
| Static reference data is read-only | Sharing is safe only for reads |
| Isolate at the right level | Per-test usually, per-worker if setup is expensive |
What are common mistakes?
Mistake 1: Hard-coded shared account
All tests use ACC-001. Parallel runs corrupt each other.
Fix: Generate unique account IDs per test.
Mistake 2: No cleanup
Test creates data but never deletes it.
After 100 runs, the database is full of test debris.
Fix: Fixtures with automatic teardown.
Mistake 3: Order-dependent tests
Test B relies on data created by Test A.
Breaks when run in parallel or in different order.
Fix: Each test creates its own data.
What are common interview follow-up questions?
- "How do you handle expensive test data setup?" Scope it per worker instead of per test, so the cost is amortized across the worker's tests.
- "What about read-only reference data?" Sharing is safe because no test mutates it. Seed it once before all tests.
- "How do you handle ephemeral environments?" Skip cleanup because the entire environment is discarded after the run.
Visual Overview
Test Data Sources
─────────────────
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Static Fixture │ │ API Generated │ │ Factory Method│
│ JSON/CSV │ │ POST /api/... │ │ create(opts) │
│ Versioned │ │ Real validation│ │ Defaults + │
│ Read-only │ │ Returns real ID│ │ overrides │
└───────────────┘ └───────────────┘ └───────────────┘
Parallel Isolation
──────────────────
Worker 1 Worker 2 Worker 3
┌────────────┐ ┌────────────┐ ┌────────────┐
│ACC-W1-001 │ │ACC-W2-001 │ │ACC-W3-001 │
│ACC-W1-002 │ │ACC-W2-002 │ │ACC-W3-002 │
│ (unique) │ │ (unique) │ │ (unique) │
└────────────┘ └────────────┘ └────────────┘
│ │ │
└───────────────┴───────────────┘
No overlap
No interference
Key Takeaway
I manage test data from multiple sources chosen by the situation: static fixtures for versioned reference data, API generation for preconditions that go through real validation, factory methods for objects with sensible defaults, and synthetic data for uniqueness. The harder half is keeping parallel tests from interfering: I isolate data per worker with unique keys, give each test independent setup and teardown, and never hard-code a shared mutable record.
The core principle is that every record a test creates carries a unique identifier, built from a UUID, timestamp, or worker index, so parallel tests never collide. Each test creates exactly the data it needs and cleans up afterward, making it self-contained and runnable in any order.
At the National Bank of India our regression ran across many parallel workers, and early on we had flaky failures because transfer tests shared a fixed account. I fixed it by generating uniquely keyed accounts per test through the API and giving each test its own setup and teardown. The flakiness disappeared.
Remember one sentence: isolate test data per worker with unique keys and independent setup and teardown so parallel tests never share mutable state.
---