v0.3 GitHub

Factories

Test data and seeding

Factories build models with default attributes for tests and seeders, like Laravel’s model factories. Implement HasFactory (in the prelude) and its definition:

rust
use rustique::prelude::*;

#[rustique::model(table = "users")]
#[derive(Debug, Clone, Default)]
pub struct User {
    pub id: i64,
    pub name: String,
    pub email: String,
    pub admin: bool,
}

impl HasFactory for User {
    fn definition(n: u64) -> Self {
        User {
            name: format!("User {n}"),
            email: format!("user{n}@example.com"),
            ..Default::default()
        }
    }
}

n is different on every call in the process (1, 2, 3, …), so columns with a unique index get unique values. For realistic names and texts, call a fake-data crate such as fake inside definition; the ORM does not ship one.

Making and creating

rust
let user = User::factory().make_one();                       // unsaved User
let users = User::factory().count(3).make();                 // Collection<User>, unsaved

let user = User::factory().create_one(&db).await?;           // saved User
let users = User::factory().count(3).create(&db).await?;     // Collection<User>, saved
  • create and create_one insert each model with save, so hooks and observers run and keys and timestamps are filled.
  • create stops at the first error and keeps the models it saved before. Pass a Transaction for all or nothing, or use db.rollback_after(...) in tests (see Transactions).
  • A factory is reusable: every make / create call builds new models with new values of n.

States and sequences

state changes every model after its definition. sequence also gets the model’s index in the batch, from 0. They run in the order they were added.

rust
let admin = User::factory().state(|u| u.admin = true).make_one();
assert!(admin.admin);

let users = User::factory()
    .count(4)
    .sequence(|u, i| u.admin = i % 2 == 0)              // admin, user, admin, user
    .make();
assert_eq!(users.iter().filter(|u| u.admin).count(), 2);

Named states, Laravel’s ->suspended(), are plain functions or an extension trait on Factory<User>:

rust
use rustique::Factory;

pub trait UserFactory {
    fn admin(self) -> Self;
}

impl UserFactory for Factory<User> {
    fn admin(self) -> Self {
        self.state(|u| u.admin = true)
    }
}

assert!(User::factory().admin().make_one().admin);

Relationships

Relations are named as in the model, like Laravel’s magic methods.

Has: children of each created model

rust
let users = User::factory()
    .count(2)
    .has(Post::factory().count(3), "posts")              // has_one / has_many: sets posts.user_id
    .has(Image::factory(), "images")                     // morph_one / morph_many: sets the id and type
    .has_attached(Role::factory().count(2), "roles", vec![("level", "admin".into())])  // belongs_to_many: pivot rows
    .create(&db)
    .await?;

has on a belongs_to_many relation inserts pivot rows without extra columns; has_attached sets pivot columns on each. Pivot timestamps are filled as by attach. Children are created only by create, never by make. has_many_through is not supported: create the intermediate models.

For: the parent of every model

rust
let users = User::factory().count(3).for_(&team, "team").create(&db).await?;   // existing parent

let posts = Post::factory()                                                    // a new parent
    .count(3)
    .for_factory(User::factory().for_factory(Team::factory(), "team"), "user")
    .create(&db)
    .await?;
  • for_ takes an existing model and works with make too. On a morph_to relation it sets the id and type columns (the parent’s MORPH_ALIAS).
  • for_factory creates one parent per create call, shared by all its models. make creates nothing: it leaves the foreign key as definition set it, unless recycle provides a parent.
  • Parent keys are set before the states, so a state can still change them.

Recycle: reuse existing models

rust
let teams = Team::factory().count(3).create(&db).await?;
let posts = Post::factory()
    .count(10)
    .for_factory(User::factory().for_factory(Team::factory(), "team"), "user")
    .recycle(&teams)                                     // users join these teams; no new team
    .create(&db)
    .await?;

When recycle provides models of a for_factory parent’s type, each model takes one of them in turn instead of creating a parent. recycle carries over to the factories of has and for_factory, so nested models reuse them too.

Without a relation

A state closure works for anything the relation helpers do not cover. State closures are 'static: copy what they need and move it in:

rust
let user_id = user.id;
let posts = Post::factory().count(3).state(move |p| p.user_id = user_id).create(&db).await?;

Callbacks

Callbacks every factory of a model needs go on HasFactory, like Laravel’s configure(). after_making runs on each model after its states, in make and create. after_creating runs on each saved model once the whole batch (and its has children) is saved, and gets the executor create was given as an AnyExecutor (a &Db or &Transaction, usable like either). Write it as an async fn; no Box::pin is needed, and the compiler checks that its body is Send:

rust
use rustique::AnyExecutor;

impl HasFactory for User {
    fn definition(n: u64) -> Self {
        User { email: format!("User{n}@Example.com"), ..Default::default() }
    }

    fn after_making(&mut self) {
        self.email.make_ascii_lowercase();
    }

    async fn after_creating(&mut self, db: &AnyExecutor<'_>) -> rustique::Result<()> {
        Post::factory().for_(&*self, "user").create(db).await?;
        Ok(())
    }
}

For a one-off callback, chain after_making / after_creating on the factory. They run after the model’s hooks, in the order they were added. A chained after_creating is stored, so its closure returns a boxed future:

rust
let factory = User::factory()
    .after_making(|user| user.email.push_str(".test"))
    .after_creating(|user, db| Box::pin(async move {
        user.email = format!("user{}@example.com", user.id);
        user.save(db).await
    }));

The order for each model is: definition, states, HasFactory::after_making, chained after_making; then, after saving, HasFactory::after_creating, chained after_creating.