Rust Interview Mastery · Chapter 11 11

Writing Automated Tests

cargo test, assert macros, panic-based testing, and the unit/integration split.

7 questions 2 topics Intermediate

Test Anatomy

Show a complete Rust test and explain every part.
#[cfg(test)]
mod tests {
    use super::*;              // pull in the code under test

    #[test]
    fn it_adds_two() {
        assert_eq!(2 + 2, 4);
    }
}

#[cfg(test)] compiles the module only for cargo test — zero cost in release. #[test] marks a test fn; the runner executes each on its own thread and reports pass/fail. use super::* reaches the parent module's items — that's how tests touch private code.

How do tests actually run — threads, output capture, ordering?

cargo test compiles a test binary listing all #[test] fns and runs them in parallel threads by default — so tests must not share mutable state (files, env vars) unless serialized.

Flags after -- go to the test binary: cargo test -- --test-threads=1 runs serially; -- --nocapture shows println! output (normally captured on pass); cargo test name filters by substring; cargo test -- --ignored runs only #[ignore]d tests.

assert! vs assert_eq! vs assert_ne! — and what do the extra args do?
assert!(r.can_hold(&other));
assert_eq!(4, result, "expected 4, got {:?}", result);   // PartialEq + Debug required
assert_ne!(5, result);

assert! takes a bool; assert_eq!/assert_ne! use ==/!= — so compared values need PartialEq (and Debug to print on failure; derive both). The trailing args are a format!-style failure message — put expected/actual context in it, since a bare "assertion failed" debug loop wastes time.

Panic Tests, Result Tests & Organization

How do you test that code panics? What's the expected attribute?
#[test]
#[should_panic(expected = "index out of bounds")]
fn rejects_bad_index() {
    let v = vec![1];
    let _ = v[5];
}

#[should_panic] inverts the result — the test passes iff the code panics. expected = "substring" makes it precise: the panic message must contain it, so a different panic elsewhere doesn't produce a false pass. Without expected, any panic — even from an unrelated bug — green-lights the test.

Can a test return Result instead of panicking? What's the advantage?
#[test]
fn it_works() -> Result<(), String> {
    if compute() == 4 { Ok(()) } else { Err(String::from("nope")) }
}

Result<(), E> tests let you use ? inside test bodies — great for I/O-flavored tests (fs::read_to_string(f)?). Constraint: can't combine with #[should_panic] — a Result test signals failure by Err, a panic test by panicking; pick one.

Unit tests vs integration tests — where do they live and what can they see?

Unit tests — inline #[cfg(test)] mod tests inside each source file. They can test private functions (same module tree, use super::*). Fast, white-box, cover logic details.

Integration tests — tests/ directory at project root. Each file compiles as a separate crate depending on your library — they only see the public API, exactly like real consumers. tests/common/mod.rs is the convention for shared helpers (submodule, not a test).

Rule: unit tests for internals, integration tests for the API contract.

Should you test private functions? What does the Rust community actually do?

Yes, mostly — and Rust makes it natural since unit tests sit in the same module and reach private items. The counterargument ("privates are implementation detail; test through public API") exists too, but the book and community norm lean pragmatic: if a private function is complex enough to deserve a test, test it directly.

The deeper answer: if a private fn needs heavy testing and the public API can't exercise it well, maybe it wants to be its own module/crate with a pub API. Use unit tests to catch bugs, not to cement a design.