Overview
Rust's unit-testing story is stdlib-minimal like
Go's: #[test] functions, assert! macros, and a
convention — the #[cfg(test)] mod tests block at the bottom of the file under test,
compiled only for tests and with access to private items. The community adds
parameterization (rstest), snapshot testing (insta) and mocking (mockall), but the
center of gravity stays plain functions asserting on plain values — the type system
having already eliminated the null/type-confusion checks other ecosystems test by hand.
Key points
#[cfg(test)] mod testsbeside the code is the unit-test home: private access, zero cost in shipping builds.use super::*;is the standard opener.- Tests can return
Result:fn parses() -> Result<(), ParseError>lets?replace unwrap-chains — cleaner failure paths than panicking asserts. - Assertion macros are the API:
assert!,assert_eq!/assert_ne!(with a format-args trailing message),#[should_panic(expected = "…")],matches!for enum-shape checks. pretty_assertions upgradesassert_eq!failures to colored diffs — a one-line dev-dependency almost every project carries. - rstest brings parameterized cases (
#[case(…)]) and fixtures — Rust's answer to JUnit's@ParameterizedTestand Go's table-driven idiom (plain loops over structs work too and remain common). - insta snapshots:
assert_snapshot!/assert_json_snapshot!store expected output in.snapfiles,cargo insta reviewapproves diffs — the golden-file workflow with tooling, ideal for rendered output, error messages, IR dumps. - Doctests are unit tests too: every
///example runs undercargo test— put the canonical usage example there instead of duplicating it in a test module — see rustdoc. - Doubles are trait-shaped: define the dependency as a trait, hand-write an
in-memory fake, inject via generics (
fn new(clock: impl Clock)) — monomorphized, no dynamic dispatch needed. mockall generates expectation-based mocks (#[automock]) for interaction-heavy boundaries; the hand-rolled-fake-first culture matches Go's, not Mockito's. - Time and randomness are dependencies: inject a clock trait or use
tokio::time::pause()in async tests — sleeping in tests is a flake factory in any language. - Run it with nextest;
cargo test -- --nocapturewhen you need println output;#[ignore]+-- --ignoredfor the slow shelf.
Examples
pub fn parse_duration(s: &str) -> Result<Duration, ParseError> { /* … */ }
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case("15s", Duration::from_secs(15))]
#[case("1h30m", Duration::from_secs(5400))]
fn parses_valid(#[case] input: &str, #[case] want: Duration) {
assert_eq!(parse_duration(input).unwrap(), want);
}
#[test]
fn rejects_garbage() {
assert!(matches!(parse_duration("soon"), Err(ParseError::Invalid(_))));
}
}
Related
- Testing in Rust — strategies & tooling map — parent map.
- Property-based testing & fuzzing — generated inputs where example cases run out.
- cargo-nextest — the runner.
- rustdoc & docs.rs — doctests as the third unit-test location.
- TDD — the discipline the fast loop serves.
- Unit testing in Java and Unit testing in Go — the cross-language contrasts; Rust sits with Go's minimalism, plus doctests.