Factories¶
A test that needs "a user" shouldn't have to spell out every non-null column just to get one. A factory is a recipe for a valid model: it fills in sensible fake data for every field, so your test overrides only the one or two attributes it actually cares about. The same recipes seed a development database with realistic data. Define the recipe once; call it a thousand times.
Subclass Factory, set model, and implement definition() (use self.faker for fake data):
from arvel.database import Factory
class UserFactory(Factory[User]):
model = User
def definition(self):
return {"name": self.faker.name(), "email": self.faker.unique.email()}
await UserFactory().create() # one persisted User
user = UserFactory().make(name="Ada") # one unsaved instance, with an override
UserFactory().raw() # just the attribute dict (no model)
count(n) returns a batch whose make/create return lists; state layers overrides (a dict or
callable(attrs) -> dict); sequence cycles values across the batch:
await UserFactory().count(3).create() # 3 persisted users
UserFactory().make_many(3) # 3 UNSAVED instances (list)
await UserFactory().state({"admin": True}).create() # composable; immutable (returns a copy)
await UserFactory().count(2).sequence({"name": "Alice"}, {"name": "Bob"}).create()
Resolution order is definition() → state (in order) → sequence[i] → explicit overrides.
Model.factory()¶
A Factory subclass registers itself for its model as soon as it's defined (import it — a
seeder or test module that never imports PostFactory never gets it registered), so
Model.factory() resolves it by convention:
class PostFactory(Factory[Post]):
model = Post
def definition(self): return {"title": self.faker.sentence()}
await Post.factory().create() # same as await PostFactory().create()
await Post.factory().count(3).create()
Override the convention lookup with an explicit __factory__ when you need a non-default factory:
Relationship factories¶
has/for_ wire up related rows — both only take effect on create/create_many (there's no
persisted parent row to point at from make). Both derive the foreign-key column from the named
relation method on the model (belongs_to/has_many/has_one's own convention), or take an
explicit foreign_key= override:
# belongs-to: create (or reuse) the parent, then set the child's FK
post = await PostFactory().for_(UserFactory(), "user").create()
# has-many: after creating the parent, create `count` related rows with the FK set
user = await UserFactory().has(PostFactory(), "posts", 3).create()
user = await UserFactory().has(PostFactory().count(3), "posts").create() # same thing
# belongs-to-many: create related rows AND their pivot rows (pass pivot column values)
user = await UserFactory().has_attached(RoleFactory(), "roles", {"level": 2}).create()
recycle reuses given instance(s) for any for_ needing that model class, instead of creating a
new parent each time — handy when many factories should share one parent:
admin = await UserFactory().create(role="admin")
posts = await PostFactory().recycle(admin).for_(UserFactory(), "user").count(5).create()
# all 5 posts belong to `admin`; UserFactory().create() is never actually called
after_making/after_creating run a callback (sync or async) on each built/persisted instance, in
the order registered:
await (
PostFactory()
.after_making(lambda p: setattr(p, "slug", slugify(p.title)))
.after_creating(lambda p: search_index.add(p))
.create()
)
In a test¶
The payoff shows up in a test: state the one thing under test, let the factory handle the rest.
async def test_admins_can_publish():
admin = await UserFactory().create(role="admin")
post = await PostFactory().for_(admin, "author").create(published=False)
await publish(post, actor=admin)
assert (await post.fresh()).published is True
Nothing here spells out the user's email or the post's body — the factory's definition() supplied
valid values, and the test reads as a statement of intent, not a pile of setup.
Common mistakes & gotchas¶
- A factory that's never imported. Registration happens at class-definition time. If a test uses
Post.factory()but nothing importedPostFactory, the lookup fails — import it (afactories/__init__.pythat imports each one is the usual fix). has/for_onmake. Relationship wiring only runs oncreate/create_many— there's no persisted parent formaketo point a foreign key at.- Non-unique fake data.
self.faker.name()can repeat; useself.faker.unique.email()for columns with a unique constraint, or asequencefor guaranteed-distinct values across a batch. - Mutating a factory in place.
state/count/sequencereturn a copy — assign or chain the result; the original factory is unchanged.
See also¶
- Migrations & Schema — seeders and
db:seed, where factories generate bulk rows. - Relationships — the relation methods
has/for_derive foreign keys from. - Testing — the test harness factories are built for.