Test Organization
Test organization determines how unit tests, sub-module test suites, shared test utilities, and external integration tests are structured across a Rust project so that test names remain navigable and build times stay fast.
my_crate/
├── Cargo.toml
├── src/
│ ├── lib.rs <-- Crate root + top-level #[cfg(test)] mod tests
│ ├── lexer.rs <-- Module + inline #[cfg(test)] mod tests
│ └── parser/
│ ├── mod.rs <-- Declares `#[cfg(test)] mod tests;`
│ └── tests.rs <-- Out-of-line unit tests for `parser`
└── tests/
└── public_api_test.rs <-- External integration tests
Inline vs Out-of-Line Unit Test Modules
When a module's unit tests are short (under ~200 lines), placing #[cfg(test)] mod tests { ... } inline at the bottom of the file keeps the implementation and its tests together.
When a module has hundreds of lines of unit tests or large test fixtures, move the test module into a dedicated tests.rs file using an out-of-line module declaration:
// Inside src/parser/mod.rs (or src/parser.rs):
#[cfg(test)]
mod tests; // Loads src/parser/tests.rs ONLY during `cargo test`
Because src/parser/tests.rs is still a child submodule of parser, it enjoys the exact same access to parser's private functions and fields via use super::*; as an inline mod tests block.
Nested Submodules for Grouping Related Unit Tests
Inside a large mod tests, you can create nested submodules to group tests by function or behavior. Each nested module path becomes part of the test's full path in cargo test output, making targeted filtering (cargo test lexer::numbers::) effortless:
pub fn parse_uint(s: &str) -> Option<u64> {
if s.starts_with("0x") {
u64::from_str_radix(&s[2..], 16).ok()
} else {
s.parse::<u64>().ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
mod decimal {
use super::*;
#[test]
fn parses_zero_and_positive() {
assert_eq!(parse_uint("0"), Some(0));
assert_eq!(parse_uint("42"), Some(42));
}
#[test]
fn rejects_negative_sign() {
assert_eq!(parse_uint("-5"), None);
}
}
mod hexadecimal {
use super::*;
#[test]
fn parses_lowercase_and_uppercase_hex() {
assert_eq!(parse_uint("0xff"), Some(255));
assert_eq!(parse_uint("0x1A"), Some(26));
}
}
}
fn main() {
println!("0xff = {:?}", parse_uint("0xff"));
}
0xff = Some(255)
Running cargo test displays the hierarchical module path for every test:
running 3 tests
test tests::decimal::parses_zero_and_positive ... ok
test tests::decimal::rejects_negative_sign ... ok
test tests::hexadecimal::parses_lowercase_and_uppercase_hex ... ok
test result: ok. 3 passed; 0 failed; 0 filtered out; finished in 0.00s
Sharing Test Utilities Across Multiple Modules in a Crate
To share mock types or test builders across unit tests in multiple different modules (src/lexer.rs, src/parser.rs, src/codegen.rs), declare a crate-internal test support module in src/lib.rs:
// In src/lib.rs:
#[cfg(test)]
pub(crate) mod test_support {
pub fn sample_source_code() -> &'static str {
"fn main() { let x = 42; }"
}
}
Any unit test anywhere in the crate can then import crate::test_support::sample_source_code().