Test Data Management
Flaky tests are rarely flaky code. They're flaky data. Learn how to manage test data so your suite is deterministic, parallel-safe, and secure.
1 The Hook — Why This Matters
A Wellington fintech team ran their 300-test suite in parallel on eight workers. Every Monday morning, three random tests failed. After two weeks of debugging, they discovered the cause: all tests shared a single test user account. When two tests tried to update the same user's profile simultaneously, one overwrote the other's data. The "flakiness" was a data collision, not a code bug.
Test data is the silent killer of automation reliability. If your data strategy is "use the same account every time," your tests will fail mysteriously, intermittently, and expensively.
2 The Rule — The One-Sentence Version
Every test must create or receive its own data, and that data must be cleaned up after the test — regardless of whether the test passes or fails.
Shared data creates hidden dependencies between tests. Hardcoded credentials create security risks. Static CSVs become stale the moment the schema changes. Dynamic, isolated, factory-generated data is the only scalable approach.
3 The Analogy — Think Of It Like...
Sharing a toothbrush in a household of ten.
It seems efficient until someone gets sick and everyone does. Test data shared across tests works the same way: one test's "cold" (bad state) infects the next test's results. Each test needs its own toothbrush — its own user, order, and session — created fresh and discarded after use.
4 Watch Me Do It — Step by Step
Here are the three pillars of professional test data management.
- Fixtures for setup and teardown
@pytest.fixture def new_user(): return {"username": "alice", "email": "alice@example.com"} @pytest.fixture def temp_database(): db = create_test_db() yield db db.drop_all() # teardown always runs - Factories for dynamic data generation
import factory from faker import Faker fake = Faker() class UserFactory(factory.Factory): class Meta: model = User username = factory.LazyAttribute(lambda x: fake.user_name()) email = factory.LazyAttribute(lambda x: fake.email()) is_active = True # Each call creates unique data user1 = UserFactory() user2 = UserFactory() - Environment variables for secrets and config
# .env (NEVER COMMIT) BASE_URL=https://staging.example.com API_KEY=sk_test_xxx # test code from dotenv import load_dotenv load_dotenv() BASE_URL = os.getenv("BASE_URL", "https://localhost:3000")
@pytest.fixture(scope="session")
def database_url(worker_id):
# Each parallel worker gets its own test database
if worker_id == "master":
db_name = "test_db_master"
else:
db_name = f"test_db_{worker_id}"
return f"postgresql://user:pass@localhost/{db_name}"
@pytest.fixture(autouse=True)
def setup_db(database_url):
# Reset tables before each test (or use transactions)
db = create_engine(database_url)
Base.metadata.drop_all(db)
Base.metadata.create_all(db)
yield db
db.dispose()
Run with pytest -n 4 to spawn four parallel workers, each with a separate database. No data collisions, no shared state, complete isolation.
| Strategy | How it works | Best for |
|---|---|---|
| Transaction rollback | Each test in a DB transaction, rolled back at end | Fast unit/integration tests |
| Database per worker | Parallel tests each get a unique database | CI with high parallelism |
| Fixture reset | Truncate tables between tests | Thorough but slower |
| UUID scoping | Prefix all data with a test-run UUID | Shared staging environments |
5 When to Use It / When NOT to Use It
✅ Use factory-generated data when...
- Running tests in parallel
- Testing CRUD operations
- You need unique identifiers per test
- Data schema changes frequently
❌ Static data is OK when...
- Testing read-only reference data (countries, currencies)
- Running a single sequential suite
- The data is truly immutable
Before you set up test data, ask:
- Will tests run in parallel? If yes, every test must have isolated data, not shared.
- How will credentials and secrets be stored and rotated (CI secrets, vaults, .env)?
- What happens to test data on failure? Is there guaranteed teardown?
- Are you using any production data? If yes, has it been anonymised under NZ Privacy Act?
6 Common Mistakes — Don't Do This
🚫 Hardcoding credentials in test scripts
I used to think: It's just a test account; no one cares.
Actually: Credentials in code end up in Git history, CI logs, and code reviews. Use environment variables, CI secret stores, or dedicated secret managers. Rotate test credentials quarterly.
🚫 Sharing mutable data between tests
I used to think: Creating a user for every test is slow and wasteful.
Actually: The time spent debugging flaky collisions costs far more than the milliseconds spent creating data. Use factories, transactions, or UUID scoping. Parallel execution without data isolation is gambling.
🚫 Forgetting teardown on failure
I used to think: If the test fails, the database record doesn't matter.
Actually: Leaked data accumulates. After 500 failed tests, your staging database is full of zombie records that break subsequent tests. Use try/finally or fixture teardown to guarantee cleanup.
When this technique fails
Test data management fails when you have stale or outdated test data that no longer reflects the current schema, when shared fixtures create dependencies between tests, or when secret rotation is not enforced and credentials leak. The worst case is when privacy regulations (like NZ's Privacy Act 2020) are violated by retaining real customer data in test environments.
7 Now You Try — Interview Warm-Up
Scenario: Two tests run in parallel. Test A creates an order for user "testuser_001." Test B deletes all orders for users matching the pattern "testuser_*." Test A fails intermittently with "Order not found" even though it just created it.
What is the root cause, and what is the fix?
The root cause:
Test B's cleanup pattern is too broad — it matches and deletes data created by Test A. The fix: scope each test's data uniquely. Use a test-run UUID prefix (e.g., testuser_{uuid}_001) or give each test its own database. Never use broad wildcard cleanup in parallel environments.
8 Self-Check — Can You Actually Do This?
Click each question to reveal the answer. If you got all three, you're ready to practice.
Q1. Why are factories better than static CSV files for test data?
Factories generate unique, dynamic data on demand. They adapt to schema changes automatically and prevent collisions in parallel execution. Static CSVs become stale and shared-data conflicts are inevitable.
Q2. What is the danger of hardcoding API keys in test code?
They get committed to version control, exposed in code reviews, and logged in CI output. Use environment variables or secret managers, and never commit .env files.
Q3. What is the fastest isolation strategy for database tests?
Transaction rollback. Each test runs inside a database transaction that is rolled back at the end. No data is persisted, so no cleanup is needed, and tests can run in parallel safely.
9 Interview Prep — Mid-Level Q&A
Interviewers ask how you've solved real test data problems. Be prepared to discuss isolation, parallelism, and security.
Q1. "Describe a test data problem you solved. What was the root cause?"
I've debugged flaky tests caused by shared test data. Two tests created invoices with the same customer ID, and when one test deleted all invoices for that customer, it broke the other test. I fixed it by giving each test a unique UUID prefix and isolating cleanup to only that test's data. The suite became 100% reliable in parallel.
Q2. "Where should API keys and database passwords live in your test suite?"
Never in version control. For local development, use a .env file (in .gitignore
Q3. "How do you test code that uses production data without violating privacy?"
Use synthetic data first; it covers 95% of cases. For the 5% where you need realistic distributions, use anonymised or masked subsets of production data. Under NZ's Privacy Act 2020, personally identifiable information must be removed before it leaves the secure environment. I've used tools like Faker to generate realistic but fictional data that passes the same validations as real data.
Q4. "Your test suite runs in parallel but is intermittently failing with 'foreign key constraint violation.' What do you check?"
First, ensure each test has isolated data (separate database, transaction, or UUID scope). Second, verify teardown runs even on failure (use try/finally or fixture scopes). Third, check for orphaned records from previous failed tests. Fourth, look for hardcoded IDs that assume sequential test execution. I would add logging to track which test created which record, then use that to debug the collision.