AI Tools Compared

Claude Code excels at generating database test fixtures that are maintainable, realistic, and properly isolated. This guide covers how to use Claude Code effectively for creating fixture factories, seeding strategies, and ensuring test isolation across your test suite.

Why Database Test Fixtures Matter

Database test fixtures provide the foundational data your tests need to run reproducibly. Poorly designed fixtures lead to flaky tests, hard-to-debug failures, and maintenance nightmares. Claude Code can help you generate fixtures that are both realistic and reliable.

Good fixtures share several characteristics: they represent real-world data patterns, are independent of each other, can be created and torn down quickly, and clearly document their purpose through naming and structure.

The most common failure mode for fixture-heavy test suites is state leakage — one test’s writes pollute the next test’s reads. The strategies in this guide address that directly, with transaction rollback patterns that eliminate leakage without the performance cost of rebuilding the database from scratch on every test.

Comparing Fixture Approaches

Before choosing a fixture strategy, understand the tradeoffs:

Approach Speed Isolation Realism Maintenance
Static SQL dumps Fast restore Poor (shared state) High High (manual updates)
Factory Boy factories Medium Excellent (per-test) High Low (schema-driven)
Transaction rollback Very fast Excellent High Very low
Fresh database per module Slow Perfect High Medium
In-memory SQLite Fastest Excellent Low (dialect gaps) Low

For most Python projects using SQLAlchemy, the combination of Factory Boy factories with transaction rollback fixtures provides the best balance of speed, isolation, and realism. Claude Code generates this pattern reliably when given your SQLAlchemy model definitions.

Creating Fixture Factories with Claude Code

Fixture factories generate test data on-demand rather than relying on static SQL dumps. Claude Code excels at building these patterns for various ORMs and databases.

Key benefits of factory-based fixtures:

PostgreSQL Factory Example

import factory
from factory.alchemy import SQLAlchemyModelFactory
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import Base, User, Order

engine = create_engine("postgresql://test:test@localhost/testdb")
Session = sessionmaker(bind=engine)

class UserFactory(SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session = Session()
        sqlalchemy_session_persistence = "commit"

    id = factory.Sequence(lambda n: f"user_{n}")
    email = factory.LazyAttribute(lambda obj: f"{obj.id}@example.com")
    username = factory.Faker('user_name')
    created_at = factory.LazyFunction(lambda: datetime.utcnow())
    is_active = True
    role = 'customer'

class OrderFactory(SQLAlchemyModelFactory):
    class Meta:
        model = Order
        sqlalchemy_session = Session()
        sqlalchemy_session_persistence = "commit"

    id = factory.Sequence(lambda n: f"order_{n}")
    user = factory.SubFactory(UserFactory)
    total = factory.Faker('pydecimal', left_digits=4, right_digits=2, positive=True)
    status = 'pending'
    created_at = factory.LazyFunction(lambda: datetime.utcnow())

Using Claude Code for Fixture Generation

Claude Code generates accurate fixture factories from model definitions. The most effective workflow is to open the models file in your Claude Code session and prompt directly:

Read models.py and generate Factory Boy factories for User, Order, and Product.
Use SubFactory for foreign key relationships. Include Faker providers for
realistic data. Add a pytest fixture that creates one of each with a related
set of OrderItems.

Claude Code reads your actual field types and constraints, so it generates appropriate Faker providers (Faker('email') for email fields, Faker('uuid4') for UUID primary keys) rather than placeholder strings. It also detects NOT NULL constraints and ensures all required fields have factory values.

When your schema changes, re-run the prompt with the updated model file. Claude Code will update factory definitions to match, flagging any fields where it cannot infer an appropriate Faker provider.

Test Isolation Strategies

Test isolation prevents data leakage between tests. Each test should see a clean database state and not depend on execution order.

Database Transactions for Isolation

The most efficient approach uses database transactions that rollback after each test:

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

@pytest.fixture(scope="function")
def db_session():
    engine = create_engine("postgresql://test:test@localhost/testdb")
    Session = sessionmaker(bind=engine)
    session = Session()

    # Start transaction
    connection = engine.connect()
    transaction = connection.begin()
    options = dict(bind=connection)
    session = Session(bind=connection)

    yield session

    # Rollback transaction
    session.close()
    transaction.rollback()
    connection.close()
    engine.dispose()

This pattern ensures each test operates within its own transaction, automatically rolling back any changes when the test completes.

Fresh Database Per Test Suite

For complete isolation, create a fresh database for each test module:

@pytest.fixture(scope="module")
def fresh_database():
    admin_engine = create_engine(
        "postgresql://test:test@localhost/postgres"
    )
    test_db_name = f"test_db_{uuid.uuid4().hex[:8]}"

    with admin_engine.connect() as conn:
        conn.execute(f"CREATE DATABASE {test_db_name}")
        conn.commit()

    test_engine = create_engine(
        f"postgresql://test:test@localhost/{test_db_name}"
    )
    Base.metadata.create_all(test_engine)

    yield test_engine

    test_engine.dispose()
    with admin_engine.connect() as conn:
        conn.execute(f"DROP DATABASE {test_db_name} RESTRICT")
        conn.commit()
    admin_engine.dispose()

Step-by-Step: Setting Up Fixtures with Claude Code

Follow this workflow to build a complete fixture system for a new project:

Step 1 — Collect your models

Open Claude Code with your models.py (or equivalent) in the working directory. If using Django, include models.py from each relevant app. Claude Code needs the full model graph to generate correct SubFactory relationships.

Step 2 — Generate base factories

Prompt: “Generate Factory Boy factories for all SQLAlchemy models in models.py. Use SubFactory for ForeignKey fields. Ensure Sequence-based IDs to prevent collisions.”

Step 3 — Generate the session fixture

Prompt: “Write a pytest fixture called db_session that uses SQLAlchemy’s savepoint pattern for test isolation. The fixture should be function-scoped and roll back changes after each test.”

The savepoint (nested transaction) pattern is preferable to full transaction rollback when your application code itself opens and closes transactions:

@pytest.fixture(scope="function")
def db_session(database_engine):
    connection = database_engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)

    nested = connection.begin_nested()  # savepoint

    @event.listens_for(session, "after_transaction_end")
    def restart_savepoint(session, transaction):
        if transaction.nested and not transaction._parent.nested:
            session.expire_all()
            connection.begin_nested()

    yield session

    session.close()
    transaction.rollback()
    connection.close()

Step 4 — Wire factories to the session fixture

Update factory Meta.sqlalchemy_session to use the fixture session rather than a module-level session. Ask Claude Code: “Update the factories to accept a session parameter so they use the test’s db_session fixture instead of the global session.”

Step 5 — Generate scenario fixtures

For complex test scenarios, create named fixture sets: “Generate a pytest fixture called ecommerce_scenario that creates 3 users, 2 products, and 4 orders with line items using the factories above.”

Step 6 — Validate with a smoke test

Run pytest -v tests/test_fixtures.py to confirm factories create records correctly and rollback works. Paste any failures back to Claude Code with the stack trace.

Data Seeding Strategies

Effective seeding balances realism with practicality. Claude Code can help you generate seed data that mimics production patterns.

Population-Based Seeding

Generate realistic data distributions:

def seed_realistic_users(session, count=100):
    """Seed users with realistic distribution of attributes."""
    # 80% customers, 15% premium, 5% admin
    roles = ['customer'] * 80 + ['premium'] * 15 + ['admin'] * 5

    users = []
    for i in range(count):
        user = User(
            id=f"user_{i}",
            email=f"user{i}@example.com",
            username=f"user_{i}",
            role=random.choice(roles),
            created_at=faker.date_between(start_date='-2y', end_date='today'),
            is_active=random.random() > 0.1  # 90% active
        )
        users.append(user)

    session.bulk_save_objects(users)
    session.commit()
    return users

Best Practices

Keep your fixtures maintainable by following these principles:

First, use meaningful data. Avoid generic values like “Test User 1” when realistic data better represents production scenarios.

Second, isolate test data. Never rely on data created by other tests. Each test should create its own fixtures.

Third, clean up after yourself. Even with transaction rollbacks, ensure connections close properly and resources free up.

Fourth, version your fixtures. Keep seed data in version control and regenerate when schema changes.

Finally, document edge cases. When fixtures require special handling, add comments explaining why.

FAQ

Q: Should I use Factory Boy, pytest-factoryboy, or write custom fixtures?

Factory Boy is the industry standard for Python ORM fixtures and has first-class SQLAlchemy support. pytest-factoryboy adds pytest fixture registration on top of Factory Boy, which reduces boilerplate but adds a dependency. Start with raw Factory Boy; add pytest-factoryboy once your fixture count exceeds ~10 factories. Claude Code generates both patterns equally well.

Q: How do I handle fixtures for databases that don’t support savepoints, like MySQL with MyISAM?

Use the fresh-database-per-module approach instead of transaction rollback. Alternatively, migrate tables to InnoDB (which supports transactions and savepoints). Claude Code can generate a conftest.py that creates and drops a test database per module using CREATE DATABASE IF NOT EXISTS and DROP DATABASE via a session-scoped fixture.

Q: My factories are slow because each one commits immediately. How do I speed them up?

Set sqlalchemy_session_persistence = None on the factory Meta class and flush manually: use session.flush() instead of session.commit() within tests when you only need the object in memory with an ID. Combine this with the transaction rollback fixture for maximum speed — no commits means faster rollbacks.

Q: Can Claude Code generate fixtures for NoSQL databases like MongoDB?

Yes. Claude Code generates mongomock fixtures for MongoDB testing, using the mongomock library which provides an in-memory MongoDB implementation compatible with pymongo. For Redis, it generates fixtures using fakeredis. Prompt: “Generate pytest fixtures for MongoDB using mongomock, with factory functions for User and Session documents matching this schema.”


This article was written by theluckystrike for zovo.one

Built by theluckystrike — More at zovo.one