Testing with factories¶
yara_orm.contrib.factory is the official factory_boy integration. YaraModelFactory keeps factory_boy's whole declaration surface — Sequence, Faker, LazyAttribute, SubFactory, @factory.post_generation, traits and params — and makes persistence async: create() and create_batch() return awaitables, so your test fixtures never need asyncio.run() or nest_asyncio hacks.
factory_boy is an optional dependency:
Declaring factories¶
import factory
from yara_orm.contrib.factory import YaraModelFactory
class PublisherFactory(YaraModelFactory):
class Meta:
model = Publisher
name = factory.Sequence(lambda n: f"publisher-{n}")
class AuthorFactory(YaraModelFactory):
class Meta:
model = Author
name = factory.Faker("name")
publisher = factory.SubFactory(PublisherFactory)
class BookFactory(YaraModelFactory):
class Meta:
model = Book
title = factory.Sequence(lambda n: f"book-{n}")
author = factory.SubFactory(AuthorFactory)
Creating instances¶
book = await BookFactory.create() # persisted Book
book = await BookFactory.create(title="Dune") # overrides win
books = await BookFactory.create_batch(5) # list[Book]
create() resolves all declarations synchronously (sequences, fakers, lazy attributes) and returns a coroutine that performs the inserts. A SubFactory chain of YaraModelFactory classes is awaited depth-first, so await BookFactory.create() persists the publisher, then the author, then the book — each row's foreign key pointing at a real saved row.
create_batch(n) runs its creations sequentially, not with asyncio.gather. That is deliberate: sub-factory coroutines shared between instances and SQLite's single-writer locking make concurrent inserts from one declaration set hazardous, and sequential inserts keep Sequence counters deterministic.
Keyword values may themselves be awaitables — an unawaited create() coroutine or an already-persisted instance both work:
book = await BookFactory.create(author=AuthorFactory.create()) # awaited for you
book = await BookFactory.create(author=existing_author) # used as-is
# In a batch, an awaitable kwarg is awaited once and shared by every instance:
books = await BookFactory.create_batch(3, author=AuthorFactory.create())
Building unsaved instances¶
build() stays synchronous and touches no database:
Two caveats, both raising a clear error:
- yara-orm refuses to assign an unsaved instance to a foreign key, so building a factory whose
SubFactoryfeeds an FK raisesValueErrorunless you override the value with a saved instance or raw id:BookFactory.build(author=saved_author). - The sync build API cannot await, so passing an awaitable (e.g.
BookFactory.build(author=AuthorFactory.create())) raisesTypeErrortelling you to usecreate()instead.
Post-generation hooks¶
Hooks run after the instance is persisted, inside the awaited coroutine — they always receive a saved model with create=True. A hook that returns an awaitable gets it awaited for you, which makes many-to-many setup a one-liner:
class BookFactory(YaraModelFactory):
class Meta:
model = Book
title = factory.Sequence(lambda n: f"book-{n}")
author = factory.SubFactory(AuthorFactory)
@factory.post_generation
def tags(obj, create, extracted, **kwargs):
if create and extracted:
return obj.tags.add(*extracted) # coroutine — awaited by the factory
book = await BookFactory.create(tags=[TagFactory.create(), existing_tag])
Values handed to a hook (the extracted value and its name__param extras) are resolved first, including inside lists and tuples — the unawaited TagFactory.create() above arrives as a persisted Tag. An async _after_postgeneration override is awaited too.
With the pytest ecosystem¶
Factories are plain classes, so they compose with pytest-asyncio fixtures directly:
async def test_reading_list(db):
books = await BookFactory.create_batch(3)
assert await Book.all().count() == 3
What is (and is not) supported¶
| Feature | Status |
|---|---|
Sequence, Faker, LazyAttribute/LazyFunction, SelfAttribute, params, Trait |
Supported (resolved synchronously, as usual) |
SubFactory of another YaraModelFactory (any depth) |
Supported — awaited depth-first before the parent insert |
SubFactory of a plain factory.Factory |
Supported — built synchronously, passed through |
create() / create_batch(n) |
Awaitable; batch inserts run sequentially |
build() / build_batch(n) |
Synchronous, unsaved instances; FK sub-factories need a saved override |
@factory.post_generation, PostGenerationMethodCall |
Run after persistence; awaitable results are awaited |
Async _after_postgeneration override |
Supported — awaited with the resolved results |
stub() |
factory_boy default (no persistence involved) |
Meta.inline_args |
Not supported — yara-orm models are keyword-only |
Extra keyword arguments pass straight through to Model.create(), so using_db="replica" routes an instance to another configured connection — see Multiple databases.