Writing Toasty Tests
SkillDev toolsGuides your agent to follow the Toasty repository's conventions when writing test code.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Writing Toasty Tests skill
About this capability
Always use this skill before writing any test code in the Toasty repository
What this skill tells your AI
The instructions your AI receives, as published by tokio-rs/toasty in .agents/skills/write-tests/SKILL.md and read by ahel’s review.
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 thetoasty::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 oflet 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<()> {
// 1. Define model(s) inline
#[derive(Debug, toasty::Model)]
struct Foo {
#[key]
#[auto]
id: uuid::Uuid,
name: String,
}
// 2. Setup database
let db = t.setup_db(models!(Foo)).await;
// 3. Exercise behavior
let foo = toasty::create!(Foo { name: "hi" }).exec(&db).await?;
// 4. Assert
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 tou64anduuid::Uuidvariants#[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::Uuidwhen ID generation is not under test, including in SQL-only tests - Use
u64with#[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
// Build a Db::Builder registering a set of models
models!(Foo, Bar, Baz)
// Look up a TableId from the schema (for driver-op assertions)
table_id(&db, "foos") // -> TableId
column(&db, "foos", "name") // -> ColumnId
columns(&db, "foos", &["id", "name"]) // -> Vec<ColumnId>
Test struct methods
t.setup_db(models!(Foo)).await // create DB, push schema, return Db
t.capability() // &'static Capability (sql, auto_increment, …)
t.log().clear() // clear driver operation log
t.log().pop() -> (Operation, Response) // inspect what the driver received
t.log().is_empty() // assert no extra driver calls
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! wins — single field
assert_eq!(foo.name, "hello");
// assert_struct! wins — multiple fields in one call
assert_struct!(foo, _ { name: "hello", age: 30 });
// vs. assert_eq!(foo.name, "hello"); assert_eq!(foo.age, 30);
// assert_eq! wins — you have the whole value
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, .. }); // dot-path shorthand
assert_struct!(val, _ { items.len(): 3, .. }); // method call
Operator patterns at leaves avoid importing types:
// Instead of: field: SomeEnum::Variant(42)
// Write: field: == expected_var
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) // asserts Err, returns the error value
assert_ok!(expr) // asserts Ok, returns the inner value
assert_none!(expr) // asserts None
assert_unique!(slice) // asserts all elements are distinct
Model Attribute Reference
Common attributes used in inline test model definitions:
#[derive(Debug, toasty::Model)]
struct User {
#[key] #[auto] id: uuid::Uuid, // primary key, auto-generated
#[unique] email: String, // generates get_by_email / filter_by_email
#[index] name: String, // generates filter_by_name
#[default(0)] score: i64, // default value on create
#[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>,
}
// Composite key
#[derive(Debug, toasty::Model)]
#[key(partition = team, local = name)]
struct Player { team: String, name: String }
Common Query Patterns
// Get by PK (errors if missing)
let u = User::get_by_id(&db, &id).await?;
// Filter (returns cursor)
let users = User::filter_by_name("Alice").collect::<Vec<_>>(&db).await?;
// First (returns Option)
let opt = User::filter_by_email("x@x.com").first(&db).await?;
// Delete
u.delete(&db).await?;
User::filter_by_id(id).delete(&db).await?;
// Update (instance — mutates in memory too)
u.update().name("Bob").exec(&db).await?;
// Update (query-based)
User::filter_by_id(id).update().name("Bob").exec(&db).await?;
// Create with nested association
User::create().name("Alice").todo(Todo::create().title("T1")).exec(&db).await?;
Signals
- GitHub stars
- 3k
- Forks
- 136
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
write-tests-tokio-rs- Source
- github.com/tokio-rs/toasty