Unit Tests in Rust and Where the Test Module Lives
Put unit tests in a cfg(test) module at the bottom of the file they test. See how the test harness discovers them, the access to private items and naming conventions that read well.

The place for a unit test in Rust is right under the code. That placement is not only a style: it makes the test a child module, so it can see private items, and the cfg(test) attribute keeps it out of the shipped binary. This post shows the structure, a test that returns Result, and the habits that make failures readable. It is the first kind of test in the testing in Rust guide.
Rust 1.90, edition 2024.
The layout
pub fn parse_port(s: &str) -> Result<u16, String> {
let n: u32 = s.trim().parse().map_err(|_| format!("not a number: {s:?}"))?;
if n == 0 || n > 65_535 {
return Err(format!("port out of range: {n}"));
}
Ok(n as u16)
}
fn is_privileged(port: u16) -> bool {
port < 1024
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_plain_number() {
assert_eq!(parse_port("8080"), Ok(8080));
}
#[test]
fn trims_whitespace() {
assert_eq!(parse_port(" 443\n"), Ok(443));
}
#[test]
fn rejects_zero_and_too_large() {
assert!(parse_port("0").is_err());
assert!(parse_port("70000").is_err());
}
#[test]
fn privileged_ports_are_below_1024() {
assert!(is_privileged(80));
assert!(!is_privileged(8080));
}
}cargo test prints 4 passed. The last test calls is_privileged, which is private, and that works because the module is a child of the file's root module.
Tests that return Result
#[test]
fn reads_config() -> Result<(), Box<dyn std::error::Error>> {
let n = parse_port("9000")?;
assert_eq!(n, 9000);
Ok(())
}? replaces unwrap(), and a failure prints the error instead of a panic location. Note that #[should_panic] cannot be combined with a Result return type.
What a failing test looks like
---- tests::trims_whitespace stdout ----
thread 'tests::trims_whitespace' panicked at src/lib.rs:24:9:
assertion `left == right` failed
left: Err("not a number: \" 443\\n\"")
right: Ok(443)assert_eq! prints both values. Derive Debug on your types, or the assertion fails to compile with E0277. Failure messages for custom checks are covered in assertions in Rust.
Helpers and setup
Put shared builders in the test module as plain functions:
fn user(name: &str) -> User {
User { name: name.to_string(), active: true }
}Rust has no setUp; a function call at the top of each test does the job and keeps each test self-contained. Fixtures with drop-based cleanup are in the test fixtures approach.
Naming and size
A good test name states the behavior and the condition. Keep each test to one reason to fail: a test that asserts six unrelated things hides five of them behind the first failure. Order inside the module is free; group by function with comments.
Where unit tests stop
They do not see how modules fit together, and they depend on the internal structure, so heavy refactoring breaks them. Use integration tests for behavior that should survive internal changes. The Rust book covers the layout in test organization.


