Skip to content

Generate Bulk Test Data

Factories make it easy to seed development databases or create fixtures for end-to-end tests.

Step 1: Define factories

Factories are automatically generated based on the manager's interface. But you can customise generation by defining a nested Factory class.

When one factory should create an object pointing back to it, see Create Related Objects with Factories for RelatedFactory and per-call child overrides.

from datetime import date
from general_manager.factory import (
    lazy_measurement,
    lazy_delta_date,
    lazy_project_name,
)
from general_manager.measurement import (
    MeasurementField,
    Measurement,
)
from general_manager.interface import DatabaseInterface
from general_manager.manager import GeneralManager
from django.db.models import (
    CharField,
    DateField,
)
from typing import Optional

class Project(GeneralManager):
    name: str
    start_date: Optional[date]
    end_date: Optional[date]
    total_capex: Optional[Measurement]

    class Interface(DatabaseInterface):
        name = CharField(max_length=50)
        start_date = DateField(null=True, blank=True)
        end_date = DateField(null=True, blank=True)
        total_capex = MeasurementField(base_unit="EUR", null=True, blank=True)

        class Factory:
            name = lazy_project_name()
            end_date = lazy_delta_date(365 * 6, "start_date")
            total_capex = lazy_measurement(75_000, 1_000_000, "EUR")

The generated AutoFactory is bound to the manager's ORM interface. That interface supplies handle_custom_fields(), input_fields, format_identification(), and _parent_class, which the factory uses to inspect custom fields and wrap saved models back into managers.

Step 2: Create batches

Project.Factory.create_batch(20)
InventoryItem.Factory.create_batch(50)

Factory.create(...) and Factory.create_batch(...) save each object before AutoFactory assigns many-to-many relations, so explicit many-to-many values can populate those relations safely. Omitted blank=True many-to-many fields stay empty by default; configure field-specific create mode when an omitted many-to-many field should generate and assign values. Factory.build(...) returns unsaved model instances and skips many-to-many assignment; pass scalar and foreign-key values to build when you need an in-memory object, then save and assign many-to-many relations yourself if the test needs them.

Step 3: Customise values

Override attributes when calling the factory:

Project.Factory(name="Launch Project", start_date=date.today())

For complex scenarios, define @classmethod helpers that produce pre-wired object graphs (projects with members, factories with related measurements, etc.).

Generated foreign-key and one-to-one defaults reuse existing related rows before creating new ones. This keeps bulk generation from creating duplicate lookup data when a reusable row already exists. For factories configured with a database alias, reuse looks for related rows on that alias.

For example, suppose each delivery manager belongs to a project:

from django.db.models import CASCADE, CharField, ForeignKey

from general_manager.interface import DatabaseInterface
from general_manager.manager import GeneralManager

class DeliveryManager(GeneralManager):
    name: str
    project: Project

    class Interface(DatabaseInterface):
        name = CharField(max_length=80)
        project = ForeignKey(Project.Interface._model, on_delete=CASCADE)

If at least one Project row already exists, omitting project reuses one of those rows:

Project.Factory.create(name="Existing Project")

manager = DeliveryManager.Factory.create(name="Ava")

Add a field-specific related factory mode when the manager should always receive a newly created project instead:

class DeliveryManager(GeneralManager):
    name: str
    project: Project

    class Interface(DatabaseInterface):
        name = CharField(max_length=80)
        project = ForeignKey(Project.Interface._model, on_delete=CASCADE)

        class Factory:
            _related_factory_modes = {"project": "create"}

_related_factory_mode = "create" applies the same behavior to every generated relation on the factory. _related_factory_mode = "random" keeps the legacy foreign-key behavior that may create a new related row or reuse an existing one. Nullable/default-None relations keep their nullable behavior in default mode; create mode bypasses that shortcut when the related model has a factory.

Sequences with existing rows

AutoFactory starts factory_boy sequence counters after the target model's current row count and respects the interface database alias when counting. If a table already has one row, the first generated sequence index is 1.

Override _setup_next_sequence() when a factory needs stronger uniqueness than a row count can provide, such as parsing the highest numeric suffix already present in existing names or codes.

Use _adjustmentMethod for complex data creation

When a single factory call needs to fan out into multiple records, or when the final payload depends on derived values, define Factory._adjustmentMethod.

_adjustmentMethod receives the keyword arguments passed to the factory after relation values have been normalized. It must return either:

  • one dict[str, object] for a single record
  • a list[dict[str, object]] for multiple records

The hook receives values after AutoFactory has filled generated or declared defaults, stripped many-to-many fields from constructor kwargs, and coerced foreign-key/one-to-one values. Caller-supplied keyword arguments still take precedence over generated defaults.

Factory.create(...) validates and saves every returned record, assigns any many-to-many values after saving, then wraps the saved models back into their GeneralManager class using the interface's identification fields. If a generated object is not a Django model instance, InvalidGeneratedObjectError is raised. If the interface has no parent manager, wrapping raises MissingManagerClassError; if an identification field cannot be read from the generated model, wrapping raises MissingIdentificationFieldError. Factory.build(...) runs the same adjustment logic but returns unsaved model instances instead of manager wrappers and does not write many-to-many relations.

_adjustmentMethod return values are not schema-validated before the factory uses them. A single dictionary produces one record. A list of dictionaries produces one record per item; an empty list produces an empty result list. Other iterable shapes, non-dictionary list entries, or malformed record values are unsupported and fail later through normal Python unpacking, model assignment, validation, or save errors rather than through a custom AutoFactory exception. Lists are processed in order and AutoFactory does not add a transaction around the list, so a create-mode failure can leave earlier records saved.

Example:

from django.db.models import CharField, PositiveIntegerField
from general_manager.interface import DatabaseInterface
from general_manager.manager import GeneralManager

class Fleet(GeneralManager):
    label: str
    capacity: int

    class Interface(DatabaseInterface):
        label = CharField(max_length=64)
        capacity = PositiveIntegerField()

        class Factory:
            @staticmethod
            def _adjustmentMethod(
                *,
                label: str = "Fleet",
                capacity: int = 0,
                count: int = 1,
                **extra: object,
            ) -> list[dict[str, object]]:
                records: list[dict[str, object]] = []
                for index in range(count):
                    record = {
                        "label": f"{label}-{index}",
                        "capacity": capacity + index,
                    }
                    if "changed_by" in extra:
                        record["changed_by"] = extra["changed_by"]
                    records.append(record)
                return records
fleets = Fleet.Factory.create(label="North", capacity=10, count=3)

assert [fleet.label for fleet in fleets] == [
    "North-0",
    "North-1",
    "North-2",
]
assert [fleet.capacity for fleet in fleets] == [10, 11, 12]

Use this hook when the number of records is dynamic, when values need to be generated from a shared seed, or when the created objects must stay internally consistent. Keep _adjustmentMethod focused on shaping record dictionaries; validation still happens later through the normal model full_clean() and save flow.

Step 4: Integrate with pytest fixtures

@pytest.fixture
def project_factory() -> Callable[..., Project]:
    def _factory(**kwargs):
        return Project.Factory(**kwargs)
    return _factory

Use the fixture in tests to create data on demand.

Seed a manager landscape from the command line

Use seed_manager_landscape when you want a local or demo database to contain a minimum number of rows for one or more managers that already expose factories.

The command is explicit by default. Select managers with --manager, or pass --all to target every manager discovered by GeneralManager that has Factory.create_batch. Omitting both --manager and --all raises CommandError before discovery or seeding. When --count is omitted, each selected manager targets 1 row by default, including when using --all. When --batch-size is omitted, batches default to 100 rows per transaction; this only changes how larger missing counts are split into transactions. Manager names are class-name selection keys. If two seedable managers share the same class name, discovery fails with SeedableManagerCollisionError so the helper does not choose the wrong class; the management command reports that helper failure as CommandError.

python manage.py seed_manager_landscape \
  --manager Project \
  --manager InventoryItem \
  --count 10 \
  --target InventoryItem=50 \
  --batch-size 25

Targets are minimum totals. If Project already has 12 rows, --count 10 creates no additional projects. If InventoryItem has 20 rows, --target InventoryItem=50 creates 30 more. Target overrides use ManagerName=COUNT, require positive integer counts, and must refer to selected managers. Empty override input is treated as no overrides. Unknown override names are reported before known-but-unselected overrides, and repeated --manager values keep their first occurrence.

Use --dry-run to inspect ordering and missing dependencies without writing data:

python manage.py seed_manager_landscape --manager Project --count 10 --dry-run

Use --output-format json with --dry-run when another script needs the plan. The JSON output contains manager_name, target_count, and missing_dependencies for each ordered target.

The command orders selected managers so required non-null database relations are seeded first when both sides are selected. It does not automatically add unselected dependencies; select those managers explicitly when your factories require existing related data. Dependency discovery follows non-null ForeignKey and OneToOneField model metadata to related GeneralManager classes and ignores nullable, self, and non-relation fields. Dry-run output lists missing dependencies.

By default, seeding stops at the first failure and reports the manager, failed batch size, original error, created count, and remaining count. Use --continue-on-error to continue with later managers and receive a summary at the end. The failing manager stops after its first failed batch, but later managers continue. Each batch is committed in its own transaction, so successful batches that already committed remain in place, and the summary includes a created-count entry for every ordered target plus partial progress for the failed manager. A run with any collected failure still exits with CommandError after writing the failure summary. That summary includes the failed manager, original error, created count, remaining count, and failed batch size.

When invoking the command from Python with call_command(), use the public keyword names manager= and target=; Django stores them internally as managers and targets after parsing. Pass those values as None, strings, or string sequences; None is treated as omitted. Pass count and batch_size as integers or strings accepted by Python's int() parser. Python booleans are rejected for those integer options, non-positive parsed values raise CommandError, and boolean switches must be actual booleans. Invalid programmatic option types raise CommandError before any data is written.

Step 5: Tear down

Use database transactions or pytest's django_db(reset_sequences=True) marker to keep the test environment clean after bulk creation.