Build the mental model
Placing the `#[test]` attribute on a function tells `cargo test` to run it automatically, and "passing" means the function ran without panicking — `assert_eq!`/`assert!` macros panic on a failed condition and produce a failure message for you. Unit tests are typically written inside `src/` (via `#[cfg(test)] mod tests { ... }`), able to directly access even private functions to confirm small pieces of logic are correct, while integration tests live separately under `tests/`, calling only the public API to simulate real usage patterns. There's a practical rule of thumb for deciding between Lesson 11's `Result<T, E>` (recoverable) and `panic!` (unrecoverable): if input comes from an external source (user input, a file, the network) and might legitimately be invalid, use `Result` and hand the decision to the caller; but if code is violating an internal invariant ("this situation should never happen if the code is correct"), `panic!` is appropriate — stopping immediately is safer than risking data corruption by continuing. Library crates should minimize `panic!` and prefer `Result`, letting the library's caller decide how to handle errors — but in an application binary (`main.rs`), `.unwrap()`/`.expect()` can be a reasonable, pragmatic choice in situations where continuing would genuinely be dangerous.
Connect it to a real scenario
Write a unit test for the content analyzer's `count_words`: `#[test] fn counts_words_correctly() { assert_eq!(count_words("a b c"), 3); }` — running `cargo test` after every refactor immediately catches regressions. Write an integration test for `read_lesson` in `tests/read_lesson_test.rs`, creating an actual test fixture file on disk and calling the public API end-to-end. Calling the file path parameter with an empty string ("") represents plausibly invalid caller input, so return `Result::Err` — but if the running `total` variable's type overflows while accumulating word counts (an internal invariant that should theoretically never be violated), reach for `panic!`/`debug_assert!` instead.
Try the working example
fn count_words(text: &str) -> u32 {
text.split_whitespace().count() as u32
}
fn read_lesson_body(path: &str) -> Result<String, String> {
if path.is_empty() {
return Err(String::from("path must not be empty"));
}
Ok(format!("contents of {path}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_words_correctly() {
assert_eq!(count_words("rust ownership basics"), 3);
}
#[test]
fn rejects_empty_path() {
assert!(read_lesson_body("").is_err());
}
}Running `cargo test` reports "test result: ok. 2 passed" in the terminal.5-minute try-it
Write three `#[test]` functions for `fn classify(word_count: u32) -> &'static str` (from Lesson 5) — check the expected output for boundary values (499, 500, 1500) with `assert_eq!`.
One important caution
Assuming external input (a user-provided file path, a network response) never needs validation and using `panic!`/`.unwrap()` everywhere — a simple user typo can crash the whole program; handle it gracefully with `Result` instead.
Over-testing private implementation details (every internal helper individually) with unit tests — when tests break on every implementation change, they're testing implementation instead of behavior, making refactoring painful.
The Rust Programming Language — Writing Automated Tests — Rust