| 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(id(ID))]
pub async fn my_test(t: &mut Test) -> Result<()> {
#[derive(Debug, toasty::Model)]
struct Foo {
#[key]
#[auto]
id: ID,
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(())
}
The id(ID) argument names a placeholder identifier — ID by convention — that the macro replaces with a concrete type everywhere it appears as a type in the function body. The function is emitted twice: once with every ID replaced by u64, once with every ID replaced by uuid::Uuid. Both variants are wrapped with #[tokio::test] and each gets 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(id(ID))] — expands to two variants (u64 and uuid::Uuid); only use when it genuinely makes sense to test both ID types
#[driver_test(id(ID), requires(sql))] — ID expansion with a capability gate
#[driver_test(requires(native_decimal))] — no ID expansion, just a capability gate
#[driver_test] — single variant, no capability gate, use a concrete ID type in the model
Choosing an ID type: Only use id(ID) expansion when running against both u64 and uuid::Uuid adds meaningful coverage. Otherwise pick the type that fits the test:
- Use
uuid::Uuid when the test must run on non-SQL drivers (DynamoDB does not support auto-increment)
- Use
u64 when testing auto-increment behavior specifically
- 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: ID,
#[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: ID,
#[index] user_id: ID,
#[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?;