基于 SOC 职业分类
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/tokio-rs/toasty --skill write-tests命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | write-tests |
| description | Always use this skill before writing any test code in the Toasty repository |
Load this skill when writing or editing tests anywhere in this project.
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.
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?;.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!.
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 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 scenariosChoosing 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.
uuid::Uuid when ID generation is not under test, including in SQL-only testsu64 with #[driver_test(requires(auto_increment))] when testing auto-increment behaviorAlways use requires(...) to gate tests on capabilities. Never use runtime if !t.capability().foo { return Ok(()); } — that is what the macro is for.
crates/toasty-driver-integration-suite/src/prelude.rs re-exports everything test files need. Start new test files with use crate::prelude::*;.
// 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>
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
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());
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());
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
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.
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
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 }
// 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?;