Mid-Level Automation · Design Pattern

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.

Mid-Level Automation ISTQB CTAL-TAE v2.0 — Chapter 5 ~12 min read + exercise

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...

Analogy

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.

  1. 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
  2. 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()
  3. 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")
Parallel test isolation with pytest-xdist and worker-scoped databases
@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.

Test isolation strategies
StrategyHow it worksBest for
Transaction rollbackEach test in a DB transaction, rolled back at endFast unit/integration tests
Database per workerParallel tests each get a unique databaseCI with high parallelism
Fixture resetTruncate tables between testsThorough but slower
UUID scopingPrefix all data with a test-run UUIDShared staging environments
Pro tip: Under NZ's Privacy Act 2020, using real production data in test environments requires careful handling. Default to synthetic data for all automated tests. If you must use production subsets, mask or anonymise all personally identifiable information before it leaves production.

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

🎯 Interactive Exercise

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.