Writing automated tests
The #[test] attribute, assertions, organization, cargo test, nextest, proptest, and what to require.
Tests are first-class in Rust. The compiler runs them, Cargo organizes them, and the convention is to write them in the same file as the code they test. No separate test discovery dance, no test runner configuration, no separate dependency tree.
For an agent orchestrator, the issue is almost never "the agent did not write tests." It is "the agent wrote only happy-path tests." Every Err variant in a Result deserves a test. assert!(result.is_ok()) is a smell because it discards the actual error message on failure.
The basics
A test is a function with the #[test] attribute. cargo test discovers and runs them.
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
}#[cfg(test)] means "only compile this module when building tests." Production builds drop the entire mod tests block. The test module imports the surrounding module's private items via use super::*, so unit tests can poke at private functions.
Assertions
assert!(condition); // Panics if false.
assert_eq!(left, right); // Panics if not equal.
assert_ne!(left, right); // Panics if equal.
assert!(condition, "message: {}", v); // Custom message.Prefer assert_eq! and assert_ne! over assert!(a == b). They print both sides on failure, which is what you want to read in the test output.
#[should_panic]
For tests that verify code panics on bad input. Always include an expected string so the test does not pass when the wrong panic fires.
#[test]
#[should_panic(expected = "denominator was zero")]
fn divide_by_zero_panics() {
divide(10, 0);
}Without expected, any panic passes the test. A panic from a typo in your test setup will silently pass.
Test organization
Three places tests can live:
Unit tests in the same file as the code, inside #[cfg(test)] mod tests. They can test private functions.
Integration tests in a top-level tests/ directory. Each file becomes its own crate, only sees the public API.
my_crate/
├── src/
│ └── lib.rs
├── tests/
│ ├── api.rs # cargo test --test api
│ └── e2e.rs # cargo test --test e2eDoc tests in /// comments. Every code block in the docs is run by cargo test.
/// Adds two numbers.
///
/// ```
/// assert_eq!(my_crate::add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}Doc tests are the single best way to make sure example code in the docs never rots.
Running tests
cargo test # Run everything.
cargo test adds_two # Filter by substring.
cargo test --test api # Run one integration file.
cargo test -- --nocapture # Show println! from tests.
cargo test -- --ignored # Run #[ignore]'d tests.
cargo test -- --test-threads=1 # Single-threaded.By default tests run in parallel. Tests that share global state (env vars, current dir, sockets bound to fixed ports) need --test-threads=1 or a mutex.
cargo nextest
A drop-in test runner that is faster, has better output, and isolates failures.
cargo install cargo-nextest --locked
cargo nextest runIt runs each test in its own process, so a panic in one test does not poison threads in another. CI-friendly. Many large Rust projects default to it.
Property tests with proptest
For pure functions where the right input space is "any integer" or "any string," property tests beat hand-written examples.
use proptest::prelude::*;
proptest! {
#[test]
fn add_is_commutative(a: i32, b: i32) {
prop_assert_eq!(a.wrapping_add(b), b.wrapping_add(a));
}
}proptest generates hundreds of inputs, shrinks failing cases to a minimal reproducer, and re-runs that reproducer on subsequent runs.
Snapshot tests with insta
For tests where the expected output is a large structured value (parsed AST, query plan, serialized config), snapshot tests beat hand-written assertions.
#[test]
fn parses_select_statement() {
let parsed = parse("SELECT a, b FROM t WHERE c > 1");
insta::assert_debug_snapshot!(parsed);
}First run writes a .snap file. Subsequent runs compare. cargo insta review shows diffs interactively and accepts or rejects updates.