17 releases (7 breaking)
Uses new Rust 2024
| 0.8.5 | May 10, 2026 |
|---|---|
| 0.7.2 | May 9, 2026 |
#425 in Testing
17KB
fixtura
Declarative fake data injection for Rust tests, built on fake-rs.
Instead of this:
#[test]
fn order_belongs_to_user() {
let user: User = Faker.fake();
let mut order: Order = Faker.fake();
order.user_id = user.id;
order.status = "pending".to_string();
assert!(is_billable(&order));
assert_eq!(order.user_id, user.id);
}
Write this:
#[fixtura::test]
fn order_belongs_to_user(
user: User,
#[fixtura(user_id = user.id, status = "pending".to_string())]
order: Order,
) {
assert!(is_billable(&order));
assert_eq!(order.user_id, user.id);
}
The test signature is the setup.
Features
- Zero-boilerplate injection — declare
Dummytypes as test arguments; fixtura fakes them all - Field overrides — pin the exact values your test cares about; fixtura fakes the rest, including nested paths
- Cross-arg references — bind a later argument's fields to values from earlier arguments
- Seeded RNG — every run is reproducible; seeds print on failure and replay on demand
- Async-ready — pairs with
#[tokio::test],async-std, or any async runner via#[fixtura::inject] - Framework composable — mix with
sqlx::test,axum_test, and others using passthrough args
Installation
[dev-dependencies]
fixtura = "0.8.3"
fake = { version = "5", features = ["derive"] }
Add #[derive(Dummy)] to any type you want injected:
use fake::Dummy;
#[derive(Dummy)]
struct User {
id: u32,
name: String,
active: bool,
}
#[derive(Dummy)]
struct Order {
id: u32,
user_id: u32,
status: String,
}
Quick start
use fake::Dummy;
#[derive(Dummy)]
struct User { id: u32, name: String, active: bool }
#[fixtura::test]
fn active_user_can_login(#[fixtura(active = true)] user: User) {
assert!(user.active);
}
That's it.
Usage
Inject any Dummy type as a test argument:
#[fixtura::test]
fn user_name_is_never_empty(user: User) {
assert!(!user.name.is_empty());
}
Pin the fields your test cares about — fixtura fakes the rest:
#[fixtura::test]
fn inactive_users_cannot_checkout(
#[fixtura(active = false)] user: User,
order: Order,
) {
assert!(checkout(&user, &order).is_err());
}
Nested field paths work too:
#[fixtura::test]
fn delivery_uses_city(
#[fixtura(address.city = "Portland".to_string())] profile: Profile,
) {
assert_eq!(profile.address.city, "Portland");
}
Reference earlier arguments to keep data coherent:
#[fixtura::test]
fn order_belongs_to_user(
user: User,
#[fixtura(user_id = user.id)] order: Order,
) {
assert_eq!(order.user_id, user.id);
}
Chain references across multiple args:
#[fixtura::test]
fn line_item_links_to_user_and_order(
user: User,
#[fixtura(user_id = user.id)] order: Order,
#[fixtura(user_id = user.id, order_id = order.id)] line: LineItem,
) {
assert_eq!(line.user_id, user.id);
assert_eq!(line.order_id, order.id);
}
Async tests
Use #[fixtura::inject] alongside your async runner. It injects args without emitting #[test] — the outer attribute handles that.
Mark each arg fixtura should own with #[fixtura] or #[fixtura(...)]. Field overrides, cross-references, and #[should_panic] all work the same.
#[tokio::test]
#[fixtura::inject]
async fn payment_fails_for_inactive_user(
#[fixtura(active = false)] user: User,
#[fixtura(user_id = user.id)] order: Order,
) {
assert!(process_payment(&user, &order).await.is_err());
}
Note
#[fixtura::inject] must sit below your async runner attribute. Rust applies stacked proc-macro attributes from bottom to top: #[fixtura::inject] transforms the function body first; #[tokio::test] (or your runner) then wraps the result.
Framework passthrough
When combining fixtura with another injecting framework (e.g. sqlx::test), mark only the args fixtura should own. Unmarked args stay in the signature for the other framework to inject.
#[sqlx::test]
#[fixtura::inject]
async fn saves_to_db(
pool: PgPool, // sqlx owns
#[fixtura] user: User, // fixtura owns
#[fixtura(user_id = user.id)] order: Order, // fixtura owns, with override
) {
db::save_order(&pool, &user, &order).await.unwrap();
}
Reproducible failures
Every test uses a seeded RNG. The seed is printed only on failure, so passing tests stay silent:
---- order_belongs_to_user stdout ----
[fixtura] seed = 8317492031748291
Paste it back to replay the exact same values:
#[fixtura::test(seed = 8317492031748291)]
fn order_belongs_to_user(user: User, order: Order) { ... }
Tip
Pin a seed permanently for fully deterministic tests:
#[fixtura::test(seed = 42)]
fn my_test(user: User) { ... }
Works the same with #[fixtura::inject(seed = 42)].
Struct types
Plain injection works for named, tuple, and unit structs — any type that implements Dummy.
Note
#[fixtura(...)] field overrides require named struct fields. Tuple index syntax (0, 1) cannot be used as a field path because it is not a valid identifier. To pin a value, use a named struct instead:
// Tuple struct — plain injection only:
#[derive(Dummy)]
struct Point(f64, f64);
#[fixtura::test]
fn uses_point(p: Point) { let _ = p; }
// To pin a value, use a named struct:
#[derive(Dummy)]
struct Point { x: f64, y: f64 }
#[fixtura::test]
fn pinned_point(#[fixtura(x = 1.0_f64)] p: Point) {
assert_eq!(p.x, 1.0);
}
IDE support
Note
rust-analyzer provides type-checking and syntax highlighting inside #[fixtura(...)] overrides. Field name completions are not available — there is no stable mechanism for proc-macro crates to provide LSP completions inside attribute arguments. Mistyped field names surface at compile time as ordinary type errors.
Dependencies
~95–440KB
~11K SLoC