Testing
Rust's testing tools are built into Cargo. Unit tests live next to the code. Integration tests live in a sibling directory. Everything runs with cargo test. There is no test framework to pick.
Unit tests: in the same file
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_positive_numbers() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn adds_negatives() {
assert_eq!(add(-2, -3), -5);
}
}Three things to notice:
#[cfg(test)]excludes the test module from non-test builds.use super::*;imports the parent module so tests can call private functions.#[test]marks a function as a test. It must take no arguments and return()(orResult<(), E>).
This is the canonical shape. Sail uses it consistently across the workspace.
Integration tests: in tests/
my-crate/
├── src/
│ └── lib.rs
└── tests/
└── catalog.rs # Each file in tests/ is its own crate.use my_crate::CatalogManager;
#[test]
fn manager_creates_default_catalog() {
let m = CatalogManager::default();
assert_eq!(m.catalogs().count(), 1);
}Integration tests can only call your crate's public API. They prove the public surface works without being able to reach private internals.
Use integration tests for end-to-end behavior and public-API contracts. Use unit tests for internal correctness.
Async tests
#[tokio::test]
async fn handles_concurrent_requests() {
let server = start_test_server().await;
let result = server.request("hello").await.unwrap();
assert_eq!(result, "world");
}#[tokio::test] builds a runtime and runs the test on it. Substitute your async runtime's macro if you use a different one.
Assertions
| Macro | Use for |
|---|---|
assert!(cond) | Boolean condition. |
assert_eq!(a, b) | Equality (prints both sides on failure). |
assert_ne!(a, b) | Inequality. |
assert!(matches!(value, Variant(_))) | Pattern match on an enum without binding. |
Always prefer assert_eq! over assert!(a == b). The diagnostic on failure is much better.
Error-path tests
The most-neglected category. Every Err variant should have at least one test that produces it:
#[test]
fn rejects_missing_port() {
let err = parse_config("{}").unwrap_err();
assert!(matches!(err, ConfigError::MissingField { .. }));
}
#[test]
fn rejects_invalid_json() {
let err = parse_config("{not json").unwrap_err();
assert!(matches!(err, ConfigError::Json(_)));
}This is one of the 12 failure modes — agents test the happy path and skip the error paths.
Test helpers
Use #[cfg(test)] modules to expose helpers only during testing:
#[cfg(test)]
pub(crate) mod test_helpers {
pub fn dummy_user() -> super::User {
super::User { id: 1, name: "test".into() }
}
}Now test_helpers::dummy_user() is available in tests but not in production builds.
Property tests
For invariant-rich code, use proptest:
use proptest::prelude::*;
proptest! {
#[test]
fn round_trips(n in 0u64..) {
let bytes = n.to_le_bytes();
let back = u64::from_le_bytes(bytes);
prop_assert_eq!(n, back);
}
}proptest generates many random inputs and shrinks failing ones to the minimal case. For codecs, parsers, math, this is a huge force multiplier.
Snapshot tests
For output that is verbose but stable, use insta:
use insta::assert_snapshot;
#[test]
fn formats_query_plan() {
let plan = build_plan();
assert_snapshot!(plan.to_string());
}cargo insta review opens a TUI to accept or reject snapshot changes. Useful for query plan rendering, code generation, error messages, large structures.
Running tests
cargo test # Run all
cargo test catalog # Run tests whose name matches "catalog"
cargo test -- --nocapture # Don't capture stdout
cargo test -- --test-threads=1 # Run serially (for tests with shared state)
cargo nextest run # Parallel test runner with better output (recommended)For production projects, install cargo-nextest. It is faster, runs tests in separate processes (so a panic doesn't kill the runner), and gives much better failure output.
Doc tests
Code inside doc comments runs as tests:
/// Add two numbers.
///
/// ```
/// assert_eq!(my_crate::add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}cargo test --doc runs all doc tests. Worth using for crate examples; the test ensures the example actually compiles.
The gates
Reiterating the Cargo gates here for context:
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --workspaceIf you remember nothing else: this is the floor for any agent-written PR.