Test Data Management: Fixtures, Factories, and Data Strategies
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
- What is the difference between fixtures and factories?
- How do you ensure test data isolation?
- What cleanup strategies prevent data pollution?
- How do you handle test data in parallel test execution?
Answers:
- 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.
- 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.
- Transaction rollback (fast, no cleanup code), delete created records by tracked IDs, truncate tables between test classes, or drop/recreate test schema per session.
- 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
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