QA Interview Hub · Knowledge base · Quizzes
System Design
Test data and environments
Test data strategies
Test data is the most frequent cause of unstable and slow tests. There are several approaches, and a Senior must explain when to use which.
| Approach | Pros | Cons |
|---|
| Static data in the DB | Simple, fast | Conflicts in parallel runs, data gets "spoiled" |
| Generating through the API in the test | Isolation, realistic | Slower, depends on the API |
| A seed script or a DB snapshot | A fast environment start | Must be maintained together with the schema |
| Factories plus direct DB writes | Very fast | Bypasses business logic, can create an impossible state |
The basic rule: each test creates its own data and doesn't rely on data from other tests. Only reference data that nobody changes can be shared: currencies, countries, tariffs.
For fintech there is a nuance: creating a "ready" user often requires KYC, phone verification, opening an account. Test hooks help here: an internal endpoint, available only in test environments, that creates a verified user with one request. This must be agreed with development and closed off from production.
Data must be unique: an email with testInfo.workerIndex and a random suffix, unique account numbers. No real personal data and no real cards: only the provider's test cards.
In an interview: My principle is that each test creates the needed data itself through the API or test hooks and doesn't depend on other tests. I leave only immutable reference data shared, and I don't use real personal data in tests at all.
Task: test data for parallel runs
The condition: 64 parallel workers, a shared staging, tests of transfers between accounts. The tests periodically fail because the balance isn't what was expected.
Why it fails
Two tests use one user, "test_user_1". Test A transfers 30, and test B at that moment checks the balance of 100 and gets 70. This is a classic shared state conflict.
The solution
- Unique data per test. A fixture creates a new user and account before the test.
- A pool of accounts per worker, if creation is expensive. The worker with index N takes accounts from pool N. A worker-scoped fixture in Playwright creates them once per worker.
- A run prefix. All entities carry a run label, for example
run_8f2c_w3_. That makes it easy to find and clean up garbage after the run. - Relative checks, not absolute, if the state is shared: remember the balance before the action and compare the difference.
export const test = base.extend<{}, { account: Account }>({
account: [async ({}, use, workerInfo) => {
const acc = await createAccount(`run_${RUN_ID}_w${workerInfo.workerIndex}`);
await use(acc);
await closeAccount(acc.id);
}, { scope: 'worker' }],
});
Trade-offs
Fresh data for every test gives maximum isolation, but loads the API and the database grows. A pool per worker is faster, but tests within a worker must clean up their state.
Metrics
The number of failures caused by data, the volume of garbage in the DB after a run, the time spent preparing data in a test.
In an interview: I remove shared state: each test or each worker gets its own accounts with a run label. If creation is expensive, I use a worker-scoped fixture, and after the run I clean the data by prefix.
Environments: local, ephemeral, staging
A test system is not only the test code but also the environments where they run.
Local (docker compose) -> a fast test development loop
Ephemeral env per PR -> isolated verification of a change
Shared staging -> integration with real dependencies
Production (smoke, synthetic) -> a check after release
An ephemeral environment is an environment brought up for every PR (for example a namespace in Kubernetes) and destroyed after the merge. The plus: no conflicts between teams, clean data. The minus: cost and spin-up time, and it is hard to bring up all 40 microservices.
The compromise: bring up only the changed service and its closest dependencies, and take the rest from the shared staging or replace them with stubs.
External dependencies. A payment provider, a KYC service and bank APIs often have a sandbox with limitations: rate limits, instability, the inability to reproduce certain errors. So for most tests I put in a stub at the HTTP level (WireMock or similar) with controllable responses: success, refusal, timeout, a duplicate callback. I keep the real sandbox for a small set of integration tests that run separately and don't block the PR.
In an interview: I separate environments by purpose: local for development, ephemeral per PR for isolation, staging for integration and smoke in production. External payment services I mostly replace with controllable stubs, to reproduce errors and timeouts, and I use the real sandbox in a separate small set.