| name | write-tests |
| description | Always use this skill before writing any test code in the Toasty repository |
Writing Toasty Tests
Load this skill when writing or editing tests anywhere in this project.
Where to Write Tests
Prefer integration tests over lib (inline #[cfg(test)]) tests. Choose the location that best matches what is being tested:
| What you're testing | Where to put it |
|---|
| Full-stack behavior requiring a real DB | crates/toasty-driver-integration-suite/src/tests/ |
| Full-stack behavior with no operational DB needed | tests/tests/ |
Public API of toasty-core | crates/toasty-core/tests/ |
SQL generation in toasty-sql | crates/toasty-sql/tests/ |
| Complex internal logic (e.g., query simplification) | Inline #[cfg(test)] in the relevant source file |
Never write tests for macro codegen internals directly. Test macro behavior at the user level (define a model, use the generated code) in the integration suite or tests/tests/.
Never write per-driver tests. Any test that exercises a real DB goes in toasty-driver-integration-suite so third-party driver authors can run it too. The suite is instantiated per driver in tests/tests/{sqlite,mysql,postgresql,dynamodb}.rs.
Test Style
- Succinct over thorough-looking: Each test function should focus on one thing. Code inside the test body should be the core behavior under test — nothing more.
- DRY the scaffolding: Use or write helpers for setup that is not what the test is testing. If you find yourself copy-pasting non-essential setup across tests, extract a helper.
- Use
create! for data setup: Unless the test is explicitly testing the create builder itself, use the toasty::create! macro to insert data. It is more concise and reads like a struct literal. For example: let user = toasty::create!(User { name: "Alice" }).exec(&db).await?; instead of let user = User::create().name("Alice").exec(&db).await?;.
- assert_struct! vs assert_eq!: Use whichever produces fewer characters and the same coverage. See the decision rule below.
Integration Suite Tests
Tests in crates/toasty-driver-integration-suite/src/tests/ run against every supported driver. New files are auto-discovered at compile time via generate_test_registry!.
Anatomy of a suite test
use crate::prelude::*;
#[driver_test]
pub async fn my_test(t: &mut Test) -> Result<()> {
#[derive(Debug, toasty::Model)]
struct Foo {
#[key]
#[auto]
id: uuid::Uuid,
name: String,
}
let db = t.setup_db(models!(Foo)).await;
let foo = toasty::create!(Foo { name: "hi" }).exec(&db).await?;
assert_eq!(foo.name, "hi");
Ok(())
}
#[driver_test] emits one test using the model's concrete ID type.
The id(ID) argument names a placeholder identifier — ID by convention —
that the macro replaces everywhere it appears as a type in the function body.
The UUID expansion adds no ID-specific capability requirement. The u64
expansion implicitly requires auto_increment. An explicit requires(...)
expression applies to both expansions. Both variants get an isolated table
prefix that is cleaned up after the test runs.
This is why FK fields are also typed ID (e.g. user_id: ID): the substitution is textual across the whole function, so the FK gets the same concrete type as the primary key it references.
Attribute forms:
#[driver_test] — single variant, no capability gate, use a concrete ID type in the model
#[driver_test(requires(native_decimal))] — no ID expansion, just a capability gate
#[driver_test(scenario(crate::scenarios::two_models))] — one concrete UUID scenario
#[driver_test(requires(auto_increment), scenario(crate::scenarios::has_many_belongs_to::id_u64))] — one concrete auto-increment scenario
#[driver_test(id(ID))] — expands to u64 and uuid::Uuid variants
#[driver_test(id(ID), requires(sql))] — ID expansion with a capability gate
#[driver_test(id(ID), scenario(crate::scenarios::has_many_belongs_to))] — matching UUID and u64 scenarios
Choosing an ID type: Use id(ID) only when UUID and auto-increment IDs
exercise different code paths or assertions. Examples include generated-value
retrieval, serialized column positions, and associations that consume a
generated ID. Otherwise write the concrete ID type in the model.
- Use
uuid::Uuid when ID generation is not under test, including in SQL-only tests
- Use
u64 with #[driver_test(requires(auto_increment))] when testing auto-increment behavior
- Use a string, manual integer, composite key, etc. when that is what the test is about
Always use requires(...) to gate tests on capabilities. Never use runtime if !t.capability().foo { return Ok(()); } — that is what the macro is for.
Prelude
crates/toasty-driver-integration-suite/src/prelude.rs re-exports everything test files need. Start new test files with use crate::prelude::*;.
Key helpers
models!(Foo, Bar, Baz)
table_id(&db, "foos")
column(&db, "foos", "name")
columns(&db, "foos", &["id", "name"])
Test struct methods
t.setup_db(models!(Foo)).await
t.capability()
t.log().clear()
t.log().pop() -> (Operation, Response)
t.log().is_empty()
Inspecting driver operations
Capture and assert on the raw operation the engine sent to the driver:
t.log().clear();
foo.update().name("new").exec(&db).await?;
let (op, resp) = t.log().pop();
if t.capability().sql() {
assert_struct!(op, Operation::QuerySql(_ {
stmt: Statement::Update(_ {
target: UpdateTarget::Table(== foo_table_id),
assignments: #{ 1: _ { expr: == "new", .. } },
..
}),
..
}));
} else {
assert_struct!(op, Operation::UpdateByKey(_ {
table: == foo_table_id,
keys.len(): 1,
assignments: #{ 1: _ { expr: == "new", .. } },
..
}));
}
assert_struct!(resp, _ { rows: Rows::Count(1), .. });
assert!(t.log().is_empty());
Assertion Macros
assert_struct! rule
Use whichever form produces fewer characters and the same test coverage.
assert_eq!(foo.name, "hello");
assert_struct!(foo, _ { name: "hello", age: 30 });
assert_eq!(result, Foo::default());
assert_struct! quick reference
Patterns compose freely. Use _ for wildcard struct (no import needed), .. for partial match:
assert_struct!(val, _ { field: "text", count: > 0, flag: true, .. });
assert_struct!(val, _ { opt: Some(42), res: Ok("ok"), .. });
assert_struct!(val, _ { items: [1, 2, ..], tags: #("a", "b", ..), .. });
assert_struct!(val, _ { nested.child.x: >= 0, .. });
assert_struct!(val, _ { items.len(): 3, .. });
Operator patterns at leaves avoid importing types:
Full pattern grammar: ~/.cargo/registry/src/index.crates.io-*/assert-struct-*/LLM.txt
assert_struct! formatting
rustfmt does not format assert_struct! bodies. You are responsible for formatting them manually, using the same rules rustfmt would apply to equivalent Rust code.
One-liner: If the entire call fits on one line within the column limit (~100 chars), keep it on one line.
assert_struct!(resp, _ { rows: Rows::Count(1), .. });
Multi-line: When breaking across lines, treat the macro body like a struct literal or match arm — indent the contents by 4 spaces relative to the assert_struct! call, and put the closing }); on its own line.
assert_struct!(op, Operation::QuerySql(_ {
stmt: Statement::Update(_ {
target: UpdateTarget::Table(== foo_table_id),
assignments: #{ 1: _ { expr: == "new", .. } },
..
}),
..
}));
When the macro syntax differs from Rust (e.g. #{ .. } for map patterns, == expr leaf operators, dot-path keys like items.len():), use your best judgment to apply equivalent rustfmt rules: align commas consistently, indent nested levels, and never leave trailing whitespace.
std-util assertion macros
assert_err!(expr)
assert_ok!(expr)
assert_none!(expr)
assert_unique!(slice)
Model Attribute Reference
Common attributes used in inline test model definitions:
#[derive(Debug, toasty::Model)]
struct User {
#[key] #[auto] id: uuid::Uuid,
#[unique] email: String,
#[index] name: String,
#[default(0)] score: i64,
#[has_many] todos: toasty::HasMany<Todo>,
}
#[derive(Debug, toasty::Model)]
struct Todo {
#[key] #[auto] id: uuid::Uuid,
#[index] user_id: uuid::Uuid,
#[belongs_to(key = user_id, references = id)]
user: toasty::BelongsTo<User>,
}
#[derive(Debug, toasty::Model)]
#[key(partition = team, local = name)]
struct Player { team: String, name: String }
Common Query Patterns
let u = User::get_by_id(&db, &id).await?;
let users = User::filter_by_name("Alice").collect::<Vec<_>>(&db).await?;
let opt = User::filter_by_email("x@x.com").first(&db).await?;
u.delete(&db).await?;
User::filter_by_id(id).delete(&db).await?;
u.update().name("Bob").exec(&db).await?;
User::filter_by_id(id).update().name("Bob").exec(&db).await?;
User::create().name("Alice").todo(Todo::create().title("T1")).exec(&db).await?;