Factories
Generate realistic test data by building in-memory instances or persisting them through the ORM, the factory_boy way.
Factories
umbral-testing's Factory trait produces realistic model instances in tests (one row, an overridden row, or a whole batch) without hand-writing the same struct literal everywhere. It's the factory_boy / FactoryBot shape in Rust, layered on the fake crate for believable values.
It pairs with the test client: factories generate the rows, the test client exercises the handler against them. For hand-edited JSON seed data, umbral-core's load_fixture / dump_fixture (in crates/umbral-core/src/fixtures.rs) round-trip rows through the ORM.
umbral-testing is a dev-dependency utility crate, not a plugin. It never ships in a release build. Add it under [dev-dependencies].
The marker shape
A factory is a small marker type with an associated Model, not an impl on the model itself. That's deliberate: in a test crate both your model and the Factory trait are foreign, so impl Factory for Plugin would violate Rust's orphan rule, but impl Factory for PluginFactory (a local marker) compiles fine.
use umbral_testing::{Factory, seq};use umbral_testing::fake::{Fake, faker::{lorem::en::Sentence, company::en::CompanyName}}; struct PluginFactory; impl Factory for PluginFactory { type Model = Plugin; fn build() -> Plugin { let mut p = Plugin::default(); p.name = CompanyName().fake(); p.slug = format!("plugin-{}", seq()); // unique per call p.short_description = Sentence(4..8).fake(); p }}build() is pure: a fresh, unsaved instance with realistic values (its id stays whatever your model's default() leaves it, typically 0, until create() persists it). You only write type Model = Plugin; and build(); everything else is provided.
Creating rows
The create* methods persist through the ORM against the ambient pool, so a built app (App::builder()…build()) must be in scope and the tables must exist.
let one = PluginFactory::create().await?; // one rowlet many = PluginFactory::create_batch(5).await?; // five rowslet featured = PluginFactory::create_with(|p| p.featured = true).await?;build()
create()
create_with(tweak)
create_batch(n)
Unique columns: seq()
seq() is a process-wide monotonic counter. Use it for any unique column (slug, email, crate name) so a create_batch doesn't trip the UNIQUE constraint:
slug: format!("plugin-{}", seq()), // "plugin-1", "plugin-2", …Realistic submissions, end to end
Simulate a realistic submission, then drive the actual flow with the test client:
#[tokio::test]async fn directory_lists_a_factory_built_plugin() { // App::builder()…build() sets the ambient pool + registers models. boot().await; let plugin = PluginFactory::create_with(|p| { p.moderation = PluginModeration::Approved; p.source = PluginSource::Official; }).await.unwrap(); let client = TestClient::new(router()); client.get("/plugins").await .assert_status_ok() .assert_body_contains(&plugin.name);}See also
- Test client: boot the app and assert on responses.
- JSON seed files:
load_fixture/dump_fixtureincrates/umbral-core/src/fixtures.rs. - Design and rationale:
planning/features.md#79 / #52.