Skip to content

Test Data Management: Fixtures, Factories, and Data Strategies

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Test Data Management: Fixtures, Factories, and Data Strategies. We cover key concepts, practical examples, and best practices to help you master this topic.

Test data management involves creating, seeding, isolating, and cleaning up data for API tests using fixtures, factories, data builders, and strategies to ensure deterministic, independent, and maintainable test data.

What You'll Learn

How to manage test data effectively: use fixtures for static reference data, factories for dynamic data generation, seed databases for integration tests, clean up data between tests, version test data, and manage data in CI/CD pipelines.

Why It Matters

Poor test data management causes flaky tests, slow test suites, and false failures. DodaTech's data management Strategy ensures each test starts with known data, modifies it deterministically, and cleans up completely.

Real-World Use

A DodaTech test suite runs 200 API tests. Each test uses a Factory to create unique users, orders, and products. Tests don't share data — each operates on its own dataset. Cleanup removes all test data, leaving the database in its original state.

flowchart LR
    A["Test\nSuite Start"] --> B["Run\nFixture Setup"]
    B --> C["Test 1\nFactory Data"]
    B --> D["Test 2\nFactory Data"]
    C --> E["Run Test\n+ Assert"]
    D --> F["Run Test\n+ Assert"]
    E --> G["Cleanup\nTest 1 Data"]
    F --> H["Cleanup\nTest 2 Data"]
    G --> I["Next\nTest"]
    H --> I
    style A fill:#dbeafe,stroke:#2563eb
    style G fill:#fecaca,stroke:#dc2626
    style H fill:#fecaca,stroke:#dc2626

Fixtures (Static Data)

# fixtures.py - static data loaded once
import json
import os

FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures")


def load_fixture(name):
    """Load a static fixture file."""
    path = os.path.join(FIXTURES_DIR, f"{name}.json")
    with open(path) as f:
        return json.load(f)


# fixtures/users.json
"""
[
    {"id": 1, "email": "alice@example.com", "name": "Alice", "role": "admin"},
    {"id": 2, "email": "bob@example.com", "name": "Bob", "role": "user"},
    {"id": 3, "email": "charlie@example.com", "name": "Charlie", "role": "user"}
]
"""

# Usage in tests:
def test_get_admin_users(api_client):
    # Seed known admin user from fixture
    seed_users(load_fixture("users"))
    response = api_client.get("/users?role=admin")
    assert len(response.json()) == 1
    assert response.json()[0]["email"] == "alice@example.com"

Factories (Dynamic Data)

# factories.py - dynamic data generation
import factory
from factory import Faker, Sequence, SubFactory

class UserFactory(factory.Factory):
    class Meta:
        model = dict

    id = Sequence(lambda n: n + 100)  # Start at 100 to avoid fixture IDs
    email = Faker("email")
    name = Faker("name")
    role = "user"
    created_at = Faker("iso8601")

class AdminFactory(UserFactory):
    role = "admin"

class OrderFactory(factory.Factory):
    class Meta:
        model = dict

    id = Sequence(lambda n: n + 1000)
    user_id = Sequence(lambda n: n + 100)
    product = Faker("word")
    quantity = Faker("random_int", min=1, max=5)
    total = Faker("pydecimal", left_digits=3, right_digits=2, positive=True)
    status = "pending"

# Usage in tests:
def test_create_order(api_client):
    user = UserFactory()
    seed_user(user)  # Insert user into test database

    order_data = OrderFactory(user_id=user["id"])
    response = api_client.post("/orders", json=order_data)

    assert response.status_code == 201
    assert response.json()["user_id"] == user["id"]

def test_admin_has_access(api_client):
    admin = AdminFactory()
    seed_user(admin)

    response = api_client.get("/admin/dashboard")
    assert response.status_code == 200

# Generate unique test data on the fly
@pytest.fixture
def unique_user():
    return UserFactory(email=f"test-{uuid.uuid4().hex[:8]}@example.com")

Database Seeding

# seed.py - database seeding for integration tests
class TestDataSeeder:
    def __init__(self, db_connection):
        self.db = db_connection

    def seed_default_data(self):
        """Seed reference data required by all tests."""
        self.db.execute("""
            INSERT INTO roles (id, name) VALUES (1, 'admin'), (2, 'user')
        """)
        self.db.execute("""
            INSERT INTO categories (id, name)
            VALUES (1, 'Electronics'), (2, 'Books'), (3, 'Clothing')
        """)
        print("Default data seeded")

    def seed_users(self, count=10):
        """Seed a specified number of test users."""
        users = [UserFactory() for _ in range(count)]
        for user in users:
            self.db.execute(
                "INSERT INTO users (email, name, role) VALUES (?, ?, ?)",
                (user["email"], user["name"], user["role"])
            )
        print(f"Seeded {count} users")
        return users

    def seed_orders(self, user_ids, orders_per_user=3):
        """Seed orders for given users."""
        orders = []
        for uid in user_ids:
            for _ in range(orders_per_user):
                order = OrderFactory(user_id=uid)
                self.db.execute(
                    "INSERT INTO orders (user_id, product, quantity, total, status) "
                    "VALUES (?, ?, ?, ?, ?)",
                    (order["user_id"], order["product"],
                     order["quantity"], order["total"], order["status"])
                )
                orders.append(order)
        print(f"Seeded {len(orders)} orders")
        return orders

    def cleanup_all(self):
        """Remove all test data."""
        self.db.execute("DELETE FROM orders")
        self.db.execute("DELETE FROM users")
        # Keep reference data (roles, categories)
        print("Test data cleaned up")

Data Cleanup Strategies

import pytest

# Strategy 1: Transaction rollback
@pytest.fixture
def db_transaction(connection):
    """Use database transaction that rolls back after test."""
    connection.begin()
    yield connection
    connection.rollback()


# Strategy 2: Dedicated test schema
@pytest.fixture(scope="session")
def test_schema(connection):
    """Create a dedicated test schema and drop it after the session."""
    connection.execute("CREATE SCHEMA IF NOT EXISTS test_data")
    yield "test_data"
    connection.execute("DROP SCHEMA test_data CASCADE")


# Strategy 3: Unique prefix per test
@pytest.fixture
def unique_test_id():
    """Generate a unique prefix for each test's data."""
    import uuid
    return f"test_{uuid.uuid4().hex[:8]}"


# Strategy 4: Factory-based cleanup
@pytest.fixture
def tracked_data():
    """Track all created records for cleanup."""
    created = {"users": [], "orders": [], "products": []}
    yield created
    # Cleanup in reverse dependency order
    for table in ["orders", "products", "users"]:
        ids = created[table]
        if ids:
            # Bulk delete
            pass  # delete from {table} where id in {ids}


# Strategy 5: Parallel-safe data isolation
@pytest.fixture
def isolated_db(db_pool, worker_id):
    """Each parallel worker gets its own database schema."""
    schema = f"test_{worker_id}"
    db_pool.execute(f"CREATE SCHEMA IF NOT EXISTS {schema}")
    yield schema
    db_pool.execute(f"DROP SCHEMA {schema} CASCADE")

Common Mistakes

1. Sharing Data Between Tests

Tests that depend on the same database rows, files, or global state interfere with each other. Each test must create its own data or use isolated data sets.

2. Hardcoded IDs in Tests

Using ID 1, 2, 3 in assertions causes failures when seed order changes or parallel tests insert conflicting records. Generate IDs dynamically or look them up by known fields.

3. Not Cleaning Up Test Data

Leftover test data accumulates in development and CI databases, slowing queries and causing unique constraint violations. Always clean up in teardown, even on test failure.

4. Slow Data Seeding

Seeding 10,000 users before every test is slow. Seed reference data once per session (fixtures), and create only the specific data each test needs (factories).

5. Using Production Data in Test Environments

Production data contains PII (emails, addresses, payment info). Never copy production data to test environments. Use anonymized or synthetic data that mimics production volume and distribution.

Practice Questions

  1. What is the difference between fixtures and factories?
  2. How do you ensure test data isolation?
  3. What cleanup strategies prevent data pollution?
  4. How do you handle test data in parallel test execution?

Answers:

  1. Fixtures are static predefined data loaded from files (reference data, lookup tables). Factories dynamically generate unique data per test call, with randomized fields and configurable overrides.
  2. Use unique data per test (UUID prefixes, factory-generated unique emails), database transactions that rollback, dedicated schemas per worker, or clean up all created records in teardown.
  3. Transaction rollback (fast, no cleanup code), delete created records by tracked IDs, truncate tables between test classes, or drop/recreate test schema per session.
  4. Use separate database schemas per worker (test_worker_1, test_worker_2), unique ID generation per test, and factory-sourced data that doesn't conflict across workers.

Challenge: Build a complete test data management system for an e-commerce API: fixtures for reference data (categories, roles, tax rates), factories for dynamic entities (users, orders, products), database seeder with configurable counts, cleanup strategies (transaction rollback + tracked IDs), parallel worker isolation, and benchmark seed performance.

FAQ

Should I use UUIDs or auto-increment IDs for test data?

UUIDs are better for test data — they're globally unique, don't conflict in parallel runs, and don't depend on insertion order. Auto-increment IDs work for sequential single-threaded tests.

How much data should I seed for tests?

Seed the minimum needed for the test to pass. Reference data once per session. Test-specific data: 1-10 rows per entity. For performance tests, seed realistic volumes (1000+ rows).

How do I manage test data for E2E tests?

Create test data via the API itself (register user, create product) rather than direct database inserts. This validates the API while setting up data. Clean up via API DELETE endpoints.

What data should I keep in version control?

Fixture files (JSON, YAML) belong in version control. Factory definitions belong in code. Database seed scripts belong in version control. Generated test data (.db files, temp files) should be gitignored.

How do I handle time-sensitive test data?

Use relative dates (today, yesterday, tomorrow) instead of hardcoded dates. Use dynamic date generation in factories: created_at = datetime.now() - timedelta(days=random.randint(1, 30)).

Mini Project

Build a test data management system: define fixtures for reference data (3 categories), factories for 5 entity types (user, product, order, payment, review), database seeder with configurable counts, cleanup strategies (transaction + tracked IDs), parallel worker isolation, and a benchmark script to measure seed/cleanup performance.

What's Next

CI/CD Testing — integrate automated API tests into your CI/CD pipeline.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro