Factories produce randomized model instances for tests and seeders. The
shape is Laravel's: UserFactory::new().count(10).create_many().await?.
The contract is one trait plus a fluent builder, with a #[derive(Factory)]
shortcut for the common case where the model already has a sensible
randomized representation.
This chapter covers defining factories by hand and by derive, composing
overrides into reusable "states", deterministic IDs via Sequence, the
Persistable seam that powers create, and the difference between
make (in-memory) and create (persisted). For the test-writing context
where factories are most useful, see Testing.
The Factory trait
The trait has exactly one required method:
definition() returns a fully populated model with every field
randomized to whatever default makes sense. The trait carries no
per-instance state - implementors are typically zero-sized markers
(struct UserFactory;) so a caller can reach the factory by name
without holding a handle.
The trait also provides two builder entrypoints with default implementations:
; // count = 1, no overrides
; // sugar for new().count(n)
Every other method you'll call (with, count, make, create,
create_many, …) lives on FactoryBuilder<M>.
Defining a factory by hand
The minimal hand-written form pairs a marker struct with a Factory
impl that knows how to build one instance. You'll typically reach for
this when the model doesn't derive fake::Dummy - perhaps because some
fields need deterministic seeding (relation IDs in a known range) or
the randomized representation needs business-rule awareness:
use Factory;
use crateUser;
;
The __eager and __pivot fields are the eager-load and pivot scratch
state that the #[suprnova::model] macro injects on every Eloquent
struct. Always default them - they get populated by the query builder,
not by factories.
next_seq() is whatever you want it to be - a static AtomicU64, a
Sequence (covered below), or a thread-local counter. The point is
that definition() runs fresh on every call inside make_many /
create_many, so any uniqueness you need has to come from a counter
the function can reach.
#[derive(Factory)] for the common case
When the model itself implements fake::Dummy - either via
#[derive(Dummy)] or a hand-written impl Dummy<Faker> for Model -
the derive collapses the marker + impl into one line on the model:
use ;
The derive emits pub struct PostFactory; as a sibling type and an
impl Factory for PostFactory whose definition() calls
Faker.fake::<Post>(). Visibility on the factory mirrors visibility
on the model - a pub model gets a pub factory, a pub(crate)
model gets a pub(crate) factory. The #[dummy(crate_name = ...)]
attribute tells Dummy where the fake crate is; see
Hand-written Dummy.
Overriding the generated name
By default #[derive(Factory)] emits <Model>Factory. Override via
the name attribute:
The value must parse as a Rust identifier - name = "User Factory"
or name = "user-factory" fails to compile with a clear span-pointed
error. The macro emits pub struct <Name>; literally, so anything
that can't be a type name can't be a factory name.
Hand-written Dummy for richer randomization
#[derive(Dummy)] works for primitive-typed structs but gives you no
control over distributions or cross-field invariants. For anything
non-trivial, write the Dummy impl by hand and pair it with
#[derive(Factory)]:
use Rng;
use ;
use Factory;
The fake crate is re-exported as suprnova::fake so consumers
don't need a separate fake = "…" line in Cargo.toml. It carries the
fakers of suprnova::fake::faker and suprnova::fake::rand::Rng for
fake_with_rng. Common types are also re-exported under the crate
root: suprnova::{Dummy, Fake, Faker}.
If you do add fake to your own dependencies, keep its version the same
as the one the framework uses. Two versions are two separate pairs of
Dummy and Fake traits, and a value that implements one does not
implement the other.
#[derive(Dummy)] generates code that names the crate ::fake. An
application without fake among its own dependencies has no such crate,
so tell the derive where it is:
use ;
Why #[derive(Factory)] only takes plain structs
The derive rejects enums, unions, and generic models with a clear
compile error. Enums and unions don't have a meaningful default
representation. Generics would force a decision about how the factory
type parameterizes its model - and there's no good default, so the
derive refuses to guess. Write the impl Factory by hand for those
cases.
The fluent builder
Factory::new() / Factory::times(n) return a FactoryBuilder<M>.
Every operation is chainable; nothing happens until you call a
terminal method (make, make_one, make_many, create,
create_one, create_many).
count(n) - how many instances
let user = new.make; // 1 user
let users = new.count.make_many; // 10 users
let same = times.make_many; // identical
count(n) is ignored by make / create (always one) and honored by
make_many / create_many. times(n) is just sugar for
Self::new().count(n) and matches Laravel's Factory::times($n).
with(|m| { … }) - per-call overrides
with registers a closure that runs against every produced instance
after definition(). Multiple with calls compose in registration
order, so a later override clobbers an earlier one on the same field:
let admin = new
.with
.with
.make;
Overrides are stored as Box<dyn Fn(&mut M) + Send + Sync + 'static>
so the builder stays Send - important for the async create /
create_many paths, which hold the builder across an .await on the
SeaORM insert.
prepend(|m| { … }) - defaults that callers can still override
prepend inserts a closure at the front of the override chain, so
it runs before any other with(...). Use it inside a state method
when you want to provide a default the caller can still clobber with a
later .with(...):
// Caller wins on `role` because their .with() comes after the prepends.
let owner = admin
.with
.make;
This is Suprnova's equivalent of Laravel's Factory::prependState. It's
the right primitive for state methods specifically - with would lose
to a caller's .with(...), which is the opposite of what a default
should do.
when(cond, |b| { … }) - conditional chaining
when threads a flag through a chain without breaking the fluent
style. The closure receives the builder, returns the builder. When
cond is false, the builder passes through unchanged:
times
.with
.when
.create_many
.await?;
Mirrors Laravel's Conditionable::when($cond, $cb). The
FnOnce(Self) -> Self signature means you can await inside the
closure as long as you .await before returning the builder.
Terminal methods
| Method | Returns | Persisted? |
|---|---|---|
make() |
one M |
no |
make_one() |
one M (forces count = 1) |
no |
make_many() |
Vec<M> of count items |
no |
create() |
Result<M, FrameworkError> |
yes |
create_one() |
Result<M, FrameworkError> (forces count = 1) |
yes |
create_many() |
Result<Vec<M>, FrameworkError> |
yes |
make_one and create_one are useful when a state method has set
count internally to something else and the caller wants exactly one
result:
// Test only wants one - `create_one` discards the count(5).
let admin = admins_in_org.create_one.await?;
States: reusable preset combinations
Suprnova doesn't ship a state("name") lookup table. Instead, states
are plain methods on your factory marker that return a pre-configured
FactoryBuilder<M>. The pattern composes by inheritance - every state
method returns the same FactoryBuilder<M> type, so you can chain more
methods onto the result:
use FactoryBuilder;
use crateUser;
;
// Compose at the call site too - chain more overrides freely.
let user = admin
.with
.create
.await?;
let batch = inactive.count.create_many.await?;
The prepend choice is deliberate: a state's overrides are defaults
that the caller can still rewrite. If you want a state's setting to be
non-negotiable, use with instead - it goes to the end of the chain
and wins.
Why no state("name") lookup
A name-keyed state registry would force runtime string matching for
something the compiler can check. State methods give you compile-time
verification (typo UserFactor::admn() is a hard error) and full IDE
autocomplete. The composability - chaining Self::admin() from inside
inactive_admin() - falls out for free.
Deterministic IDs with Sequence
Sequence is a monotonic counter for seeding unique-per-call fields.
Each next() call returns 1, 2, 3, … atomically across threads:
use ;
static ORDER_IDS: Sequence = new;
;
Sequence::new() is const, so it works as a static initializer.
The counter starts at 0 and increments to 1 on first call. Use
reset() between tests if you want a clean count - the
#[suprnova_test] macro doesn't do this for you because the framework
can't know which sequences are yours:
async
Sequence uses SeqCst ordering - overkill for "give me a unique
id" but keeps reasoning trivial. If a Sequence ever shows up in a hot
path you can write your own with Relaxed.
Persistable: the seam to your storage
The create family of methods is available whenever the model
implements Persistable:
Two impls cover the models you write:
- The
#[suprnova::model]macro emitsPersistablefor the struct it generates. It runs the same insert thatModel::createruns, so a factory insert firesCreating,Saving,Created, andSavedand reaches every observer. See Lifecycle events. - A blanket impl in
factory::persistcovers every plain SeaORM model that canIntoActiveModel<ActiveModel>. It pullsDB::connection()and inserts with SeaORM directly, so no model event fires.
No per-model boilerplate; if User is a model, UserFactory::new().create()
works. The returned Self is what the insert hands back - assigned id,
defaulted columns resolved, etc.
Primary-key handling
A SeaORM IntoActiveModel impl marks every field - including the PK -
as Set(value). For factory-produced models the PK is a placeholder
(0 for AUTO_INCREMENT i64), so a straight insert collides on the
second call with a UNIQUE constraint failure.
persist_via_seaorm (the helper that backs the blanket) flips every
primary-key column to NotSet before inserting, which lets the
database assign its own id - the semantic factories actually need. The
Persistable impl of a #[suprnova::model] struct does the same for an auto-increment key. A model whose
key is not auto-increment keeps the key its factory set:
pub async
If you actually want to assign a specific id (replay test, restoring
a fixture by id), bypass the helper and call
model.into_active_model().insert(db).await directly.
Persisting against an explicit connection
persist_via_seaorm takes the connection as an argument. Useful when
you want to drive persistence against a connection that isn't the
framework's bound DB::connection() - most often a specific
sqlite::memory: handle in an integration test:
use persist_via_seaorm;
let model = new.make;
let row = persist_via_seaorm.await?;
Custom non-SeaORM backends
Because the blanket impl targets every ModelTrait type, you can't
write impl Persistable for MyOrm::Model from a downstream crate
without colliding. For non-SeaORM custom persistence (Redis, Surreal,
blob-only stores), wrap the model in a newtype and impl Persistable
on the wrapper:
use ;
use async_trait;
;
A Factory<Model = RedisCached<MyValue>> then gets create /
create_many for free.
make vs create: when to use which
make returns the model without touching the database:
// Unit test for a pure function - no DB needed.
let draft = new.with.make;
let snippet = extract_summary;
assert!;
create persists and returns the post-insert version:
// Integration test - the action needs a real row.
let post = new.create.await?;
let action = .unwrap;
let published = action.execute.await?;
assert!;
Reach for make whenever the test doesn't care that the row exists.
Reach for create when you'll query the row back, when a foreign key
needs a real id, or when you're populating fixtures for a sub-system
that reads the DB. Note that create_many persists sequentially - if
a later insert fails, the prior inserts are NOT rolled back. For a
#[suprnova::model] struct, create / create_many take the same
write path as Model::create, so inside a DB::transaction(...) closure
they join the ambient transaction. Wrap a batch in one when you need
atomicity:
use ;
DBtransaction.await?;
A plain SeaORM model uses the blanket Persistable impl, which inserts
through DB::connection() and does not join an ambient transaction.
"After-creating" behaviour
Suprnova doesn't ship a named after_creating(|m| { … }) callback. Two
patterns cover the use cases that callback exists for in Laravel:
1. The chain - do the follow-up after create/create_many:
let user = new.create.await?;
new
.with
.create
.await?;
This is the canonical pattern when one model's id needs to flow into a
follow-up insert. create returns the persisted row, so the id is
immediately available.
2. Model observers - react on the model lifecycle, not the factory:
Use Model Observers to wire post-insert
behaviour to the model itself rather than the factory. The observer
fires for User::create(...), UserFactory::new().create(), and any
other persistence path - exactly what you want when the behaviour is
"every time this row lands, do X":
use ;
;
Factory-only callbacks would invite divergence between test inserts and real inserts. Observers stay consistent across both.
Lifecycle events
A factory insert of a #[suprnova::model] struct fires the same events
as Model::create, in the same order: Creating and Saving before
the insert, Created and Saved after it. A Creating or Saving
listener sees the row as attributes. What it changes is written over the
values the factory built, and a listener that cancels aborts the insert.
The fillable filter does not apply, because the values are the
factory's own.
To insert a row without running any listener, use create_quietly() or
create_many_quietly(). Both mute the model events for the insert, as
seed::without_events does:
let user = new.create_quietly.await?;
let users = times.create_many_quietly.await?;
Seeders
Factories produce instances; seeders orchestrate them. A Seeder is a
zero-sized type with an async run that knows what to populate:
use ;
use async_trait;
use crate;
;
Register the seeder in bootstrap.rs so the per-project console
binary's db:seed command knows about it:
;
Run through the project's console binary (every scaffolded app
ships one at src/bin/console.rs):
Seeders run in registration order. Idempotency is the seeder's
responsibility - run does not snapshot or roll back, so a seeder
that inserts unconditionally produces duplicates on re-run. Use
migrate:fresh followed by db:seed for a clean slate.
Putting it together: a complete test fixture
use ;
use ;
use TestDatabase;
use crate;
use cratePublishPostAction;
describe!;
Three patterns worth pointing at:
- The author's
idflows into the post via amoveclosure inside.with(...). Captures are explicit, which keeps the relation visible at the call site. create().await.unwrap()is the test idiom - the test is allowed to panic on setup failure because a broken fixture is a broken test, not a graceful failure mode.- Factories compose with the rest of the testing surface
(
EventFacade::fake,Storage::fake,Mail::fake, …) - none of the fakes know about factories, but every test you write will use them together.
Why Suprnova diverges
Laravel's factories ship with named states (->state('admin')),
runtime sequences (->sequence(['name' => 'A'], ['name' => 'B'])),
and an afterCreating callback registered on the factory itself.
Suprnova drops all three and replaces them with Rust-shaped
primitives:
- States are methods, not strings. Compile-time typo-checking and
IDE autocomplete are both free; the only cost is "you write
pub fn admin()instead ofprotected function admin()", which is no cost at all. - Sequences are a separate primitive.
Sequencedoes one thing (atomic counter) and is reusable outside the factory surface - you can drop one into a request id generator, a workflow step counter, or a test harness without explaining what it is. - After-creating is wired to the model, not the factory. The framework already has Model Observers for exactly that purpose. Adding a parallel mechanism on the factory would make test-time behaviour and production-time behaviour diverge by construction.
The fluent surface - count(10), times(10), with, prepend,
when, make, create, create_many, make_one, create_one -
mirrors Laravel's directly, so the muscle memory ports without a
glossary.
Next
- Testing -
#[suprnova_test],TestDatabase, the fake facades that pair with factory-built fixtures. - Eloquent - model derivation, observers, the cast
pipeline that runs when
createpersists your factory output. - Migrations - the schema your factories need to
exist against; use
migrate:fresh && db:seedfor a clean fixture slate. - Database -
DB::transaction, multi-connection routing, savepoints - what to reach for whencreate_manyneeds atomicity. - Service Container - how
App::resolveandApp::makefind the action and service types your tests call into alongside factories.
