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:
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
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>, savedcreateandcreate_oneinsert each model withsave, so hooks and observers run and keys and timestamps are filled.createstops at the first error and keeps the models it saved before. Pass aTransactionfor all or nothing, or usedb.rollback_after(...)in tests (see Transactions).- A factory is reusable: every
make/createcall builds new models with new values ofn.
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.
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>:
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
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
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 withmaketoo. On amorph_torelation it sets the id and type columns (the parent’sMORPH_ALIAS).for_factorycreates one parent percreatecall, shared by all its models.makecreates nothing: it leaves the foreign key asdefinitionset it, unlessrecycleprovides a parent.- Parent keys are set before the states, so a
statecan still change them.
Recycle: reuse existing models
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:
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:
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:
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.