Rust unit testing with the built-in `cargo test` harness - `#[test]` in `#[cfg(test)] mod tests` blocks, `assert_eq!` / `assert_ne!` / `assert!` macros, `#[should_panic(expected)]`, `Result<(), E>` test returns, integration tests in `tests/`, doc tests in `///` comments, runner flags (`--test-threads=1`, `--nocapture`, `--ignored`), `#[ignore]` marking, coverage via cargo-llvm-cov / tarpaulin, and Criterion benchmarks on stable. Includes framework choice (stdlib `#[test]` is the default; rstest for 4+ parameterized case pairs or shared fixtures via references) and test-authoring conventions (inline `#[cfg(test)]` placement, assertion-macro selection, async runtime requirements). References cover rstest parametrize + fixtures and Rust mocking with mockall (`#[automock]` / `mock!`). Use for any Rust unit-test task: writing tests, testing panics or Results, doc tests, coverage gates, benchmarks, or CI wiring.
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Companion reference for rust-unit-tests. rstest adds parametrize and
fixture patterns that stdlib #[test] lacks; its tests are still
discovered and run by cargo test - no separate runner. Use when the same
input pattern repeats across many tests, setup is shared across 3+ tests,
or migrating pytest/JUnit5 habits to Rust. For single-case tests, plain
#[test] needs no extra dependency.
Per github.com/la10736/rstest:
[dev-dependencies]
rstest = "0.21"#[case]use rstest::rstest;
#[rstest]
#[case(1, 2, 3)]
#[case(0, 0, 0)]
#[case(-1, 1, 0)]
fn add_cases(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(add(a, b), expected);
}Each #[case] runs as a separate test; failures don't stop subsequent
cases. Name the cases - #[case::positive(1, 2, 3)] yields
add_cases::positive instead of the debug-hostile case_1.
use rstest::{fixture, rstest};
#[fixture]
fn db() -> Database {
Database::new_test_instance()
}
#[fixture]
fn user(db: Database) -> User { // fixtures can chain
db.create_user("alice")
}
#[rstest]
fn test_user_id(user: User) { // injected by parameter name
assert_eq!(user.id, 1);
}Customize a fixture per test with #[default(...)] on the fixture
parameter and #[with(...)] at the call site:
#[fixture]
fn user(#[default("alice")] name: &str) -> User { User::new(name) }
#[rstest]
#[case::bob("bob")]
fn test_user(#[case] expected: &str, #[with(expected)] user: User) {
assert_eq!(user.name, expected);
}#[rstest]
fn test_matrix(
#[values("alice", "bob", "charlie")] name: &str,
#[values(0, 18, 65)] age: u32,
) {
let user = User::new(name, age);
assert!(user.is_valid());
}Runs 3 × 3 = 9 combinations. #[case] and #[values] combine (each case
× each value). Watch the explosion - 5 dims × 5 values = 3125 tests;
prefer strategic cases over a full matrix.
#[rstest]
#[case(1, 2, 3)]
#[tokio::test]
async fn async_add_cases(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(add_async(a, b).await, expected);
}Async fixtures use #[future]:
#[fixture]
async fn db_async() -> Database {
Database::connect_async().await.unwrap()
}
#[rstest]
#[tokio::test]
async fn test_async(#[future] db_async: Database) {
let db = db_async.await;
assert!(db.is_connected());
}Same as plain cargo: cargo test --all-targets - rstest tests are
native cargo test citizens.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| rstest for single-case tests | Dependency for no benefit | Plain #[test] |
| Unnamed cases | case_1, case_2 in failure logs | #[case::name(...)] |
| Full matrix everywhere | Combinatorial explosion | Strategic cases |
| Mixing sync + async in one parametrize | Confusing | Separate #[rstest] blocks |