fabricate
FactoryBot-inspired test data factory for Rust + sqlx
fabricate brings the ergonomics of Ruby's FactoryBot to Rust. Define factories once, then build in-memory structs, persist directly to Postgres via sqlx, or seed through your HTTP API — all with composable traits, auto-incrementing sequences, and field-level overrides.
Table of Contents
- Why fabricate?
- Quick Start
- Core Concepts
- Persistence Modes
- Personas (Scenario Bundles)
- Defining Your Own Factory
- Built-in Factories Reference
- CLI Tool
- Architecture Overview
- Cargo Features
- Error Handling
- Contributing
- License
- Acknowledgments
Why fabricate?
Rust's test ecosystem has excellent assertion libraries, mocking tools, and property-based testing — but no standard answer for test data factories. Setting up realistic, interconnected test entities means writing dozens of builder functions by hand, managing foreign key relationships manually, and duplicating setup logic across test files.
fabricate solves this with:
- Declarative factories — define entity defaults once, reuse everywhere
- Composable traits — named modifications that stack (
"verified","premium","suspended") - Auto-incrementing sequences — unique emails, phones, names, and license plates out of the box
- Field-level overrides — override any field inline with
.set("email", json!("custom@test.com")) - Three persistence modes — build in-memory, persist to Postgres via sqlx, or seed via HTTP API
- Persona bundles — pre-composed multi-entity scenarios (rider + wallet + payment, booking flow, etc.)
- CLI tool — seed and reset test data from the command line
Comparison
| Feature | Hand-written builders | fake / faker |
fabricate |
|---|---|---|---|
| Reusable entity definitions | Manual per-test | No structure | Factories with defaults |
| Named variants (verified, suspended) | If-else chains | N/A | Composable traits |
| Unique values (emails, phones) | Manual counters | Random (collisions) | Deterministic sequences |
| Database persistence | Custom per-entity | N/A | Built-in (sqlx + HTTP) |
| Multi-entity scenarios | Large setup blocks | N/A | Personas |
| Relationships / foreign keys | Manual wiring | N/A | Associations |
Quick Start
Add fabricate to your Cargo.toml:
[]
= "0.1"
= "1"
To use direct Postgres persistence (enabled by default):
[]
= { = "0.1", = ["postgres"] }
Build your first entity:
use ;
use UserFactory;
use json;
// Create a build-only context (no database, no HTTP)
let mut ctx = FactoryContext ;
// Build a default user (in-memory only)
let user = new
.build
.unwrap;
assert!;
assert_eq!;
// Build a verified driver with a custom email
let driver = new
.with_trait
.with_trait
.set
.build
.unwrap;
assert_eq!;
assert_eq!;
assert!;
assert!;
Core Concepts
Factory and BuildableFactory
fabricate has two core traits for defining factories:
Factory — the high-level async trait with build and create methods:
BuildableFactory<T> — the lower-level trait used by FactoryBuilder, giving you control over base construction, trait registries, field overrides, and persistence:
Most factories implement BuildableFactory<T> and are used through FactoryBuilder.
FactoryBuilder (Fluent API)
FactoryBuilder provides the chainable API for constructing entities:
let user = new
.with_trait // Apply a named trait
.with_trait // Stack multiple traits
.set // Override a field
.build // Build in-memory
.unwrap;
Methods:
| Method | Description |
|---|---|
FactoryBuilder::new(factory) |
Wrap a factory in the fluent builder |
.with_trait("name") |
Apply a named trait (composable, order matters) |
.set("field", json!(value)) |
Override a specific field value |
.build(&mut ctx) |
Build in-memory (synchronous, returns Result<T>) |
.create(&mut ctx).await |
Build + persist to database or HTTP API (async) |
Traits (Composable Modifications)
Traits are named modifications that alter specific fields on an entity. They are composable — apply multiple traits and they stack in order, with later traits overriding earlier ones for the same fields.
// Single trait
let verified_user = new
.with_trait
.build
.unwrap;
// Stacked traits — "driver" overrides user_type set by defaults
let verified_driver = new
.with_trait
.with_trait
.build
.unwrap;
assert!;
assert_eq!;
To define a trait for your own factory, implement FactoryTrait<T>:
Sequences (Unique Value Generation)
Every FactoryContext includes a Sequence generator that produces unique, deterministic values. Named sequences auto-increment independently starting from 1.
// Raw sequence counter
let n = ctx.sequence; // 1, 2, 3, ...
// Built-in helpers
let email = ctx.email; // "test_rider_1@test.ridemate.app"
let phone = ctx.phone; // "+15550000001"
let name = ctx.full_name; // "Alice Smith"
Built-in sequence helpers:
| Helper | Example Output | Pattern |
|---|---|---|
ctx.email("prefix") |
test_prefix_1@test.ridemate.app |
Unique per call |
ctx.phone() |
+15550000001 |
555 area code, zero-padded |
ctx.full_name() |
Alice Smith |
Cycles through 10 first/last names |
sequences.plate() |
AA0001 |
Letter pairs + zero-padded number |
ctx.sequence("name") |
1, 2, 3... |
Raw counter for any named sequence |
Field Overrides
Override any field on any factory using .set() with a JSON value:
let user = new
.set
.set
.build
.unwrap;
assert_eq!;
assert_eq!;
Overrides are applied after traits, so they always win. Each factory defines which fields are overridable in its apply_overrides implementation.
Persistence Modes
Build (In-Memory)
The simplest mode — build entities as plain Rust structs with no side effects:
let mut ctx = FactoryContext ;
let user = new
.build
.unwrap;
// user is a TestUser struct — no DB, no network
Create via Postgres
Persist entities directly to your Postgres database using sqlx. Requires the postgres feature (enabled by default).
let pool = connect.await?;
let mut ctx = database;
let user = new
.with_trait
.create // INSERT INTO users ...
.await?;
// user.id is now a real database UUID
Each factory's persist method contains the actual SQL. For example, UserFactory executes:
INSERT INTO users (id, email, phone_number, password_hash, full_name, ...)
VALUES ($1, $2, $3, $4, $5, ...)
ON CONFLICT (email) DO UPDATE SET updated_at = $12
RETURNING id
Create via HTTP API
Seed data through your running backend's test API endpoints. Factories POST to /__test__/ routes with X-Test-Key authentication:
let mut ctx = http
.with_test_key;
let user = new
.with_trait
.create // POST http://localhost:8080/__test__/users
.await?;
HTTP mode sends JSON payloads and expects JSON responses. The X-Test-Key header authenticates requests so your test endpoints can reject unauthorized access in production.
Personas (Scenario Bundles)
Personas compose multiple factories into realistic multi-entity scenarios. Instead of manually creating a user, wallet, payment method, driver profile, and ride, call a single persona method.
use Personas;
let mut ctx = http;
// Create a rider with wallet and payment method (3 entities)
let rider = rider.await?;
println!;
// Create a complete booking scenario (7 entities)
let booking = rider_books_ride.await?;
println!;
Available Personas
| Persona | Entities Created | Description |
|---|---|---|
rider |
3 | Verified user + funded wallet + payment method |
driver |
3 | Verified driver user + verified driver profile + wallet |
driver_onboard |
2 | Unverified driver + unverified profile (needs setup) |
rider_books_ride |
7 | Rider (3) + driver (3) + accepted ride |
complete_ride |
7 | Rider + driver + completed ride with payment and ratings |
driver_posts_trip |
4 | Driver (3) + trip post (instant booking) |
seed_for_exploration |
55+ | 3 riders + 4 drivers + 2 bookings + 3 completed rides + 2 trip posts |
Selective Seeding via API
Use Personas::seed_via_api to seed specific scenarios by name:
let summary = seed_via_api.await?;
println!;
Available scenario names: rider, driver, driver-onboard, rider-books-ride, complete-ride, driver-posts-trip, full.
Defining Your Own Factory
Here is a complete example defining a factory for an Article domain (outside the built-in Ridemate factories):
use ;
use BuildableFactory;
use FactoryContext;
use ;
use Result;
use ;
use Uuid;
// 1. Define your entity struct
// 2. Define the factory
// 3. Implement BuildableFactory
// 4. Define traits
;
;
;
// 5. Use it
use FactoryBuilder;
use json;
let mut ctx = /* your FactoryContext */;
let draft = new
.build
.unwrap;
assert_eq!;
let featured = new
.with_trait
.set
.build
.unwrap;
assert_eq!;
assert!;
assert_eq!;
Built-in Factories Reference
fabricate ships with 11 factories for the Ridemate ride-sharing domain as a reference implementation and for immediate use in Ridemate projects.
| Factory | Output Type | Available Traits |
|---|---|---|
UserFactory |
TestUser |
verified, unverified, suspended, premium, driver, admin |
DriverProfileFactory |
TestDriverProfile |
verified, unverified, high_rated, new_driver, available, offline |
RideFactory |
TestRide |
requested, accepted, in_progress, completed, cancelled, with_payment, with_ratings |
TripPostFactory |
TestTripPost |
instant_book, request_to_book, recurring, with_stops, full |
TripBookingFactory |
TestTripBooking |
pending, confirmed, completed, cancelled |
PaymentMethodFactory |
TestPaymentMethod |
visa, mastercard |
WalletFactory |
TestWallet |
funded, empty |
PaymentFactory |
TestPayment |
successful, failed, refunded, pending |
RatingFactory |
TestRating |
five_star, low_rating, with_review, driver_rating, passenger_rating |
SafetyIncidentFactory |
TestSafetyIncident |
panic_button, crash_detected, resolved |
SafetyContactFactory |
TestSafetyContact |
(none) |
All built-in factories are under fabricate::ridemate::* and support all three persistence modes.
CLI Tool
fabricate includes a CLI binary for seeding and managing test data from the command line.
Installation
Or run directly from the repository:
Commands
fabricate seed — Seed test data
# Seed everything (full exploration dataset)
# Seed specific scenarios
# Seed specific personas
# Use a custom test API key
fabricate reset — Delete all test data
# Reset everything
# Reset specific scope
fabricate list — Show available factories and personas
Output:
fabricate: Available Factories & Personas
FACTORIES:
UserFactory
Traits: verified, unverified, suspended, premium, driver, admin
DriverProfileFactory
Traits: verified, unverified, high_rated, new_driver, available, offline
RideFactory
Traits: requested, accepted, in_progress, completed, cancelled, with_payment, with_ratings
...
PERSONAS (scenario bundles):
rider - User + wallet + payment method
driver - User (driver) + driver profile + wallet
driver-onboard - Driver (unverified, needs setup)
rider-books-ride - Rider + driver + accepted ride
complete-ride - Rider + driver + completed ride + payment + ratings
driver-posts-trip - Driver + trip post (carpooling)
full - All of the above (comprehensive exploration data)
Architecture Overview
┌─────────────────────┐
│ FactoryBuilder │ Fluent API: .with_trait() .set() .build() .create()
└──────────┬──────────┘
│ uses
┌──────────▼──────────┐
│ BuildableFactory<T> │ build_base() + trait_registry() + apply_overrides() + persist()
└──────────┬──────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌─────────▼───────┐ ┌─────▼──────┐ ┌───────▼────────┐
│ TraitRegistry │ │ Sequence │ │ FactoryContext │
│ FactoryTrait<T> │ │ (counters) │ │ (pool/http/ │
│ (named mods) │ │ │ │ overrides) │
└─────────────────┘ └────────────┘ └───────┬────────┘
│ persistence via
┌────────────┼────────────┐
│ │ │
┌────▼───┐ ┌─────▼────┐ ┌───▼──┐
│ sqlx │ │ reqwest │ │ None │
│ PgPool │ │ HTTP API │ │ (mem)│
└────────┘ └──────────┘ └──────┘
| Type | Role |
|---|---|
FactoryBuilder<F, T> |
Fluent builder that chains traits, overrides, and persistence |
BuildableFactory<T> |
Trait defining how to build, customize, and persist an entity |
Factory |
Higher-level trait with build, create, create_list |
FactoryContext |
Session context holding DB pool, HTTP client, sequences, and overrides |
Sequence |
Named auto-incrementing counters with helpers for emails, phones, names, plates |
TraitRegistry<T> |
Registry of named FactoryTrait<T> implementations for an entity |
FactoryTrait<T> |
A named modification (e.g., "verified") that mutates an entity |
Association |
Describes a foreign key dependency between factories |
Personas |
Pre-composed multi-entity scenario bundles |
Cargo Features
| Feature | Default | Description |
|---|---|---|
postgres |
Yes | Enables direct Postgres persistence via sqlx (FactoryContext::database()) |
To disable the default postgres feature (HTTP-only or build-only usage):
[]
= { = "0.1", = false }
Error Handling
All fallible operations return fabricate::Result<T>, which is std::result::Result<T, fabricate::Error>.
| Variant | Description |
|---|---|
Error::Build(String) |
Factory construction failed (missing config, invalid state) |
Error::Association(String) |
Associated entity could not be resolved |
Error::TraitNotFound(String) |
Requested trait is not registered on the factory |
Error::Sequence(String) |
Sequence generation error |
Error::Database(sqlx::Error) |
Database operation failed (postgres feature only) |
Error::Http(reqwest::Error) |
HTTP request to test API failed |
Error::Json(serde_json::Error) |
JSON serialization/deserialization error |
Error::Persona(String) |
Unknown persona or scenario name |
Errors include helpful context. For example, requesting a non-existent trait:
Trait 'nonexistent_trait' not registered. Available: ["verified", "unverified", "suspended", "premium", "driver", "admin"]
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b my-feature) - Make your changes
- Run tests and checks:
- Commit with a descriptive message
- Open a Pull Request
License
MIT License. See LICENSE for details.
Acknowledgments
fabricate is inspired by FactoryBot by thoughtbot, the gold standard for test data factories in the Ruby ecosystem. This project aims to bring the same developer experience to Rust.