Manual contentsEloquent ORMBrowse 103 chapters
Manual 20 min read

Eloquent Factories

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:

pub trait Factory {
    type Model;

    fn definition() -> Self::Model
    where
        Self: Sized;
}

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:

fn new() -> FactoryBuilder<Self::Model>;       // count = 1, no overrides
fn times(n: usize) -> FactoryBuilder<Self::Model>;  // 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 suprnova::Factory;
use crate::models::users::User;

pub struct UserFactory;

impl Factory for UserFactory {
    type Model = User;

    fn definition() -> User {
        let now = chrono::Utc::now();
        User {
            // `0` is a placeholder - `persist_via_seaorm` flips
            // primary-key columns to `NotSet` before inserting so
            // the database assigns the real id.
            id: 0,
            name: format!("Factory User #{}", next_seq()),
            email: format!("factory-{}@example.test", next_seq()),
            password: "factory-placeholder".into(),
            remember_token: None,
            active: true,
            created_at: now,
            updated_at: now,
            deleted_at: None,
            __eager: Default::default(),
            __pivot: None,
        }
    }
}

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 suprnova::{Dummy, Factory};

#[derive(Dummy, Factory)]
pub struct Post {
    pub id: i64,
    pub title: String,
    pub body: String,
    pub author_id: i64,
    pub is_public: bool,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,
}

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.

Overriding the generated name

By default #[derive(Factory)] emits <Model>Factory. Override via the name attribute:

#[derive(Dummy, Factory)]
#[factory(name = "AccountFactory")]
pub struct User { /**/ }

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 suprnova::__fake::rand::Rng;
use suprnova::__fake::{Dummy, Fake, Faker, faker::lorem::en::{Paragraph, Sentence}};
use suprnova::Factory;

#[derive(Factory)]
pub struct Post { /* fields … */ }

impl Dummy<Faker> for Post {
    fn dummy_with_rng<R: Rng + ?Sized>(_: &Faker, rng: &mut R) -> Self {
        let title: String = Sentence(3..7).fake_with_rng(rng);
        let body: String = Paragraph(3..6).fake_with_rng(rng);
        let author_id: i64 = (1..=50i64).fake_with_rng(rng);
        let now = chrono::Utc::now();

        Post {
            id: 0,
            author_id,
            title,
            body,
            is_public: Faker.fake_with_rng::<bool, _>(rng),
            created_at: now,
            updated_at: now,
            __eager: Default::default(),
            __pivot: None,
        }
    }
}

The fake crate is re-exported as suprnova::__fake so consumers don't need a separate fake = "…" line in Cargo.toml. Common types are also re-exported under the crate root: suprnova::{Dummy, Fake, Faker}.

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 = UserFactory::new().make();             // 1 user
let users = UserFactory::new().count(10).make_many();  // 10 users
let same = UserFactory::times(10).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 = UserFactory::new()
    .with(|u| u.active = true)
    .with(|u| u.role = "admin".into())
    .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(...):

impl UserFactory {
    /// State method - admin defaults, caller can still customise.
    pub fn admin() -> suprnova::FactoryBuilder<User> {
        Self::new()
            .prepend(|u| u.role = "admin".into())
            .prepend(|u| u.active = true)
    }
}

// Caller wins on `role` because their .with() comes after the prepends.
let owner = UserFactory::admin()
    .with(|u| u.role = "owner".into())
    .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:

UserFactory::times(10)
    .with(|u| u.active = true)
    .when(seed_admins, |b| b.with(|u| u.role = "admin".into()))
    .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:

pub fn admins_in_org(org_id: i64) -> suprnova::FactoryBuilder<User> {
    UserFactory::times(5)               // sensible default for fixtures
        .with(move |u| u.org_id = org_id)
        .with(|u| u.role = "admin".into())
}

// Test only wants one - `create_one` discards the count(5).
let admin = admins_in_org(42).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 suprnova::FactoryBuilder;
use crate::models::users::User;

pub struct UserFactory;

impl suprnova::Factory for UserFactory {
    type Model = User;
    fn definition() -> User { /**/ }
}

impl UserFactory {
    /// Inactive variant - overlays an `active: false` default.
    pub fn inactive() -> FactoryBuilder<User> {
        Self::new().prepend(|u| u.active = false)
    }

    /// Admin variant - overlays role + verified email.
    pub fn admin() -> FactoryBuilder<User> {
        Self::new()
            .prepend(|u| u.role = "admin".into())
            .prepend(|u| u.email_verified_at = Some(chrono::Utc::now()))
    }

    /// Composable: inactive admin.
    pub fn inactive_admin() -> FactoryBuilder<User> {
        Self::admin().prepend(|u| u.active = false)
    }
}
// Compose at the call site too - chain more overrides freely.
let user = UserFactory::admin()
    .with(|u| u.name = "Alice".into())
    .create()
    .await?;

let batch = UserFactory::inactive().count(20).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 suprnova::{Fake, Sequence};

static ORDER_IDS: Sequence = Sequence::new();

pub struct OrderFactory;
impl suprnova::Factory for OrderFactory {
    type Model = Order;
    fn definition() -> Order {
        Order {
            id: 0,
            number: format!("ORD-{:06}", ORDER_IDS.next()),
            total_cents: (100..=10_000).fake(),
            created_at: chrono::Utc::now(),
            __eager: Default::default(),
            __pivot: None,
        }
    }
}

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:

#[suprnova::suprnova_test]
async fn each_order_gets_a_unique_number(db: TestDatabase) {
    ORDER_IDS.reset();   // start at 1 for this test
    let orders = OrderFactory::new().count(5).create_many().await?;
    assert_eq!(orders[0].number, "ORD-000001");
    assert_eq!(orders[4].number, "ORD-000005");
}

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:

#[async_trait]
pub trait Persistable: Sized + Send {
    async fn persist(self) -> Result<Self, FrameworkError>;
}

A blanket impl in factory::persist covers every SeaORM model that can IntoActiveModel<ActiveModel> - which is every model the #[suprnova::model] macro emits. No per-model boilerplate; if User is a model, UserFactory::new().create() works.

The blanket pulls DB::connection() and inserts. The returned Self is what SeaORM hands back from the insert - 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:

pub async fn persist_via_seaorm<M, E, C>(model: M, db: &C) -> Result<M, FrameworkError>
where
    M: ModelTrait<Entity = E> + IntoActiveModel<<E as EntityTrait>::ActiveModel> + Send,
    E: EntityTrait<Model = M>,
    /* … bounds … */
    C: ConnectionTrait,
{
    let mut active = model.into_active_model();
    for pk in <<E as EntityTrait>::PrimaryKey as Iterable>::iter() {
        active.not_set(pk.into_column());
    }
    active.insert(db).await.map_err(/**/)
}

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 suprnova::factory::persist_via_seaorm;

let model = UserFactory::new().make();
let row = persist_via_seaorm(model, db.inner()).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 suprnova::{FrameworkError, Persistable};
use suprnova::async_trait;

pub struct RedisCached<T>(pub T);

#[async_trait]
impl Persistable for RedisCached<MyValue> {
    async fn persist(self) -> Result<Self, FrameworkError> {
        let client = suprnova::App::make::<RedisClient>()
            .ok_or_else(|| FrameworkError::internal("redis client not bound"))?;
        client.set(&self.0.key, &serde_json::to_vec(&self.0)?).await?;
        Ok(self)
    }
}

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 = PostFactory::new().with(|p| p.is_public = false).make();
let snippet = my_lib::extract_summary(&draft);
assert!(snippet.len() < 200);

create persists and returns the post-insert version:

// Integration test - the action needs a real row.
let post = PostFactory::new().create().await?;
let action = App::resolve::<PublishPostAction>().unwrap();
let published = action.execute(post.id).await?;
assert!(published.is_public);

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. create / create_many go through the Persistable blanket, which talks to the framework's bound DB::connection() directly - they do not join an ambient DB::transaction(...) scope. If you need atomicity across a batch of inserts, drop into the Model trait's Model::create(attrs!{...}) inside the closure (that path routes through the same executor that honours CURRENT_TX):

use suprnova::{DB, Model, attrs};

DB::transaction(|_tx| Box::pin(async move {
    for i in 0..50 {
        User::create(attrs!{
            name: format!("user-{i}"),
            email: format!("user-{i}@example.test"),
        }).await?;
    }
    Ok::<_, suprnova::FrameworkError>(())
})).await?;

"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 = UserFactory::new().create().await?;
ProfileFactory::new()
    .with(move |p| p.user_id = user.id)
    .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 suprnova::{FrameworkError, Observer, async_trait, observer};

#[observer(User)]
pub struct AuditUser;

#[async_trait]
impl Observer<User> for AuditUser {
    async fn created(&self, user: &User) -> Result<(), FrameworkError> {
        tracing::info!(user_id = user.id, "user created");
        Ok(())
    }
}

Factory-only callbacks would invite divergence between test inserts and real inserts. Observers stay consistent across both.

Seeders

Factories produce instances; seeders orchestrate them. A Seeder is a zero-sized type with an async run that knows what to populate:

use suprnova::{Factory, FrameworkError, Seeder};
use suprnova::async_trait;

use crate::factories::{PostFactory, UserFactory};

pub struct BaseSeeder;

#[async_trait]
impl Seeder for BaseSeeder {
    fn name() -> &'static str { "BaseSeeder" }

    async fn run() -> Result<(), FrameworkError> {
        // Users first - posts reference user ids in 1..=50.
        UserFactory::new().count(50).create_many().await?;
        PostFactory::new().count(200).create_many().await?;
        Ok(())
    }
}

Register the seeder in bootstrap.rs so the per-project console binary's db:seed command knows about it:

suprnova::seed::register::<crate::seeders::BaseSeeder>();

Run through the project's console binary (every scaffolded app ships one at src/bin/console.rs):

cargo run --bin console -- db:seed

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 suprnova::{App, describe, test, expect};
use suprnova::events::{EventFacade, assert_dispatched_times};
use suprnova::testing::TestDatabase;
use crate::factories::{PostFactory, UserFactory};
use crate::actions::publish_post::PublishPostAction;

describe!("PublishPostAction", {
    test!("publishes a draft post", async fn(db: TestDatabase) {
        // Arrange - an author and one draft post owned by them.
        let author = UserFactory::new()
            .with(|u| u.active = true)
            .create()
            .await
            .unwrap();

        let draft = PostFactory::new()
            .with(move |p| p.author_id = author.id)
            .with(|p| p.is_public = false)
            .create()
            .await
            .unwrap();

        // Act.
        let action = App::resolve::<PublishPostAction>().unwrap();
        let published = action.execute(draft.id).await.unwrap();

        // Assert.
        expect!(published.is_public).to_equal(true);
        expect!(published.author_id).to_equal(author.id);
    });

    test!("publishing emits exactly one event", async fn(db: TestDatabase) {
        let _guard = EventFacade::fake();
        let post = PostFactory::new().create().await.unwrap();

        App::resolve::<PublishPostAction>().unwrap()
            .execute(post.id).await.unwrap();

        assert_dispatched_times::<crate::events::PostPublished>(1);
    });
});

Three patterns worth pointing at:

  • The author's id flows into the post via a move closure 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 of protected function admin()", which is no cost at all.
  • Sequences are a separate primitive. Sequence does 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 create persists your factory output.
  • Migrations - the schema your factories need to exist against; use migrate:fresh && db:seed for a clean fixture slate.
  • Database - DB::transaction, multi-connection routing, savepoints - what to reach for when create_many needs atomicity.
  • Service Container - how App::resolve and App::make find the action and service types your tests call into alongside factories.