Skip to content

Factories

Factories generate test data with sensible defaults. bluefox-test uses factory_boy for attribute declaration and handles all database persistence with native async calls.


BaseFactory

Extends factory.Factory (not SQLAlchemyModelFactory). Subclass it, declare fields, call register().

from bluefox_test import BaseFactory, Faker, register
from users.models import User


class UserFactory(BaseFactory):
    class Meta:
        model = User

    name = Faker("name")
    email = Faker("email")
    is_active = True

create_user = register(UserFactory)

How it works

  1. factory_boy builds a Python object with fake data (no DB)
  2. create_async() adds it to the AsyncSession and calls await session.flush()
  3. await session.refresh(instance) loads server-generated defaults (timestamps, IDs)

No greenlet bridge, no sync hacks. Pure async.

create_async()

@classmethod
async def create_async(cls, _session=None, **overrides):
Parameter Description
_session Explicit session override for cross-bind factories. If None, uses the globally-bound test session.
**overrides Field overrides passed to factory_boy's .build().

Returns the persisted model instance with a real database ID.

create_batch_async()

@classmethod
async def create_batch_async(cls, size: int, **overrides) -> list:

Creates multiple instances. Each gets unique fake data.


register()

def register(
    factory_cls: type[BaseFactory],
    session_fixture: str = "db",
) -> pytest.fixture:

Turns a factory into a global pytest fixture. The fixture name is derived from the model class name:

Model name Fixture name
User create_user
UserProfile create_user_profile
HTTPLog create_http_log
APIKey create_api_key

The returned fixture yields an async callable:

# In tests:
user = await create_user(name="Hugo")

Cross-bind factories

For models in a non-default database, specify the session fixture:

create_event = register(EventFactory, session_fixture="db_analytics")

Available re-exports

These are re-exported from factory_boy for convenience:

from bluefox_test import (
    BaseFactory,      # base class for factories
    register,         # turn a factory into a pytest fixture
    Faker,            # fake data generation
    LazyFunction,     # computed field (called once per build)
    LazyAttribute,    # computed field (has access to other fields)
    Sequence,         # sequential values (0, 1, 2, ...)
    Iterator,         # cycle through values
    Trait,            # named sets of field overrides
)

Tip

SubFactory is intentionally not re-exported. Use explicit relationship passing instead.


Relationship handling

Don't use SubFactory for required relationships. Pass related objects explicitly in your tests:

src/orders/tests/factories.py
from bluefox_test import BaseFactory, Faker, register
from orders.models import Order


class OrderFactory(BaseFactory):
    class Meta:
        model = Order

    status = "pending"
    # owner is NOT declared — always pass explicitly

create_order = register(OrderFactory)
In tests
async def test_create_order(create_user, create_order):
    user = await create_user(name="Hugo")
    order = await create_order(owner=user, status="pending")
    assert order.owner_id == user.id

For optional relationships, use None as the default:

class OrderFactory(BaseFactory):
    class Meta:
        model = Order

    status = "pending"
    coupon = None  # optional relationship

Factory file pattern

src/users/tests/factories.py
from bluefox_test import BaseFactory, Faker, register
from users.models import User, UserProfile


class UserFactory(BaseFactory):
    class Meta:
        model = User

    name = Faker("name")
    email = Faker("email")
    is_active = True

create_user = register(UserFactory)


class UserProfileFactory(BaseFactory):
    class Meta:
        model = UserProfile

    bio = Faker("paragraph")
    avatar_url = "https://example.com/avatar.png"

create_user_profile = register(UserProfileFactory)

Auto-discovery

bluefox_test_setup() calls discover_and_collect_fixtures() which walks src_root for tests/factories.py files, imports them (triggering register() calls), and merges the fixtures into the returned dict.

Requirements:

  • Factory files must be named tests/factories.py
  • Every tests/ directory needs an __init__.py
  • pythonpath = ["src"] in pyproject.toml