14.xChapter 14 summary and quiz
Rust ships a test framework, so this chapter taught testing as a habit. Review, then a quiz that has you write a small suite.
Quick review
Tests buy you the confidence to change code: the compiler proves type and memory safety, but only a test can prove your logic is correct (14.1). A good test is focused, covers edge cases, is independent of other tests, and is fast; it follows arrange-act-assert, and the assert is non-negotiable. A test is a function marked #[test] that passes if it runs without panicking (14.2). The assertion macros are assert!, assert_eq!, and assert_ne! (prefer the equality ones, which print both values on failure); #[should_panic] tests that code panics as intended; and a test can return Result so you can use ? inside it.
Unit tests live in a #[cfg(test)] mod tests block in the same file as the code (14.3). #[cfg(test)] compiles them only for cargo test, so they cost nothing in release builds, and use super::*; pulls the code under test into scope. Because the test module is a child of what it tests, it can reach private functions, though the usual advice is to test mostly through the public API and reach into privates sparingly. Integration tests live in the tests/ directory, where each file is a separate crate that sees only the public API (14.4), exercising the library as a real user would, which is exactly why logic belongs in lib.rs (chapter 13's IOU paid). Doc tests are the examples in /// comments, compiled and run by cargo test so documentation can't go stale (14.5); write them as clear examples first, hide setup with #, and leave exhaustive cases to unit tests.
Running tests (14.6): a string argument filters by substring (cargo test adds), -- --show-output reveals prints from passing tests (the -- divides Cargo's args from the runner's), #[ignore] skips slow tests by default (-- --ignored runs only them), and tests run in parallel, so they must be independent (-- --test-threads=1 forces sequential as a diagnostic).
Quiz time
Question #1
Where do unit tests, integration tests, and doc tests each live, and which of them can see private functions?
Show solution
Unit tests: a #[cfg(test)] mod tests block in the same source file, and they can see private functions (child module of the code). Integration tests: the tests/ directory, each file a separate crate, seeing only the public API. Doc tests: inside /// comments on items, run as external-user code, seeing only the public API. Only unit tests can reach privates.
Question #2
This test is meant to verify that withdraw panics on an overdraft, but it passes for the wrong reasons sometimes. Improve it.
#[test]
#[should_panic]
fn overdraft_panics() {
let mut acct = Account::new();
acct.withdraw(100); // empty account
}Show solution
Add an expected message so the test only passes for the right panic:
#[test]
#[should_panic(expected = "insufficient funds")]
fn overdraft_panics() {
let mut acct = Account::new();
acct.withdraw(100);
}
With bare #[should_panic], any panic passes the test, including an unrelated bug (an index error, an overflow). expected = "insufficient funds" requires the panic message to contain that text, confirming it's the overdraft panic.
Question #3
Why must tests be independent, and what does cargo test -- --test-threads=1 help diagnose?
Show solution
Because cargo test runs tests in parallel by default, so two tests that share state (a file, a global) can interfere and fail unpredictably when run together. --test-threads=1 runs them sequentially; if a test fails in the full parallel run but passes with one thread, shared state plus parallelism is the cause. The real fix is to make the tests independent, not to leave threads at 1.
Question #4
The exercise. Given a pub fn is_palindrome(text: &str) -> bool that ignores case and spaces (so "Race car" is a palindrome), write a #[cfg(test)] mod tests block with at least four focused tests covering: a simple palindrome, a non-palindrome, the case-and-space handling, and an edge case (empty string). Assume is_palindrome("") returns true.
Show solution
pub fn is_palindrome(text: &str) -> bool {
let cleaned: String = text
.chars()
.filter(|c| !c.is_whitespace())
.map(|c| c.to_ascii_lowercase())
.collect();
let reversed: String = cleaned.chars().rev().collect();
cleaned == reversed
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_palindrome() {
assert!(is_palindrome("level"));
}
#[test]
fn not_a_palindrome() {
assert!(!is_palindrome("rust"));
}
#[test]
fn ignores_case_and_spaces() {
assert!(is_palindrome("Race car"));
}
#[test]
fn empty_string_is_palindrome() {
assert!(is_palindrome(""));
}
}
Each test is focused (one behavior), they're independent, and together they cover the easy path, the negative case, the case/space rule, and the empty-string edge case. The implementation uses iterator adapters (filter, map, rev, collect) that chapter 19 covers in full; for the test exercise, what matters is the four assert!/assert!(!...) checks. Running cargo test reports all four as tests::... passing.
Your code is now organized (chapter 13) and trustworthy (this chapter). Chapter 15 returns to the type system to remove a different kind of repetition: writing the same function over and over for different types. Generics let one definition serve many types, and the limitation they run into ("but I can't compare two values of an unknown type yet") is the exact cliffhanger that opens the traits chapter.