Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 28: Testing, Docs, and Confidence

Prerequisites

You will understand

  • #[test], #[should_panic], and integration tests
  • Doc tests as living documentation
  • Test organization: unit, integration, doc

Reading time

30 min
+ 20 min exercises
Confidence Layers

Unit, Integration, and Doctest Cover Different Risks

unit testsintegration testsdoctestsprivate logic confidencepublic contract confidencedocumentation truth confidence
Beyond Built-ins

Property Tests, Snapshots, and Trait-Based Fakes

proptestgenerated inputsinvariantsinstasnapshot outputreview changesfake implsmall traitscheap doubles

Step 1 - The Problem

Rust’s type system catches a lot, but it does not catch:

  • wrong business logic
  • incorrect boundary assumptions
  • regressions in output shape
  • integration mistakes across crates or modules

Strong Rust codebases treat tests and docs as part of API design, not as afterthoughts.

Step 2 - Rust’s Design Decision

Rust’s built-in testing story spans:

  • unit tests inside modules
  • integration tests in tests/
  • doctests in documentation

The ecosystem adds:

  • proptest for property-based testing
  • insta for snapshot testing

Rust accepted:

  • multiple test layers
  • some boilerplate around module organization

Rust refused:

  • a single monolithic testing style pretending all confidence needs are identical

Step 3 - The Mental Model

Plain English rule:

  • unit tests validate small logic locally
  • integration tests validate public behavior from outside the crate
  • doctests validate examples and documentation truth

Step 4 - Minimal Code Example

#![allow(unused)]
fn main() {
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn adds_two_numbers() {
        assert_eq!(add(2, 3), 5);
    }
}
}

Step 5 - Line-by-Line Compiler Walkthrough

  1. #[cfg(test)] means the module exists only when compiling tests.
  2. use super::*; imports the surrounding module’s items.
  3. #[test] marks a function for the test harness.
  4. cargo test builds a test binary and runs all discovered tests.

This arrangement matters because unit tests inside the module can access private implementation details, while integration tests in tests/ can only use the public API.

Step 6 - Three-Level Explanation

Unit tests sit close to the code they check. Integration tests act more like real users of the crate.

Strong test strategy often looks like:

  • unit tests for pure logic and edge cases
  • integration tests for public workflows
  • doctests for usage examples
  • snapshot tests for structured output
  • property tests for invariants that should hold across many generated inputs

Tests are how you preserve invariants the type system cannot encode. They are especially important around:

  • parsing
  • formatting
  • protocol boundaries
  • concurrency behavior
  • error surface stability

The best Rust codebases often read tests first because tests reveal intended usage and failure boundaries more directly than implementation files.

cargo test, #[cfg(test)], and Organization

Useful commands:

cargo test
cargo test some_name
cargo test -- --nocapture
cargo test -- --test-threads=1

Keep pure helper functions small enough that they are easy to unit test. Use integration tests when you care about the public contract rather than private internals.

proptest, insta, and Test Doubles

Property testing is valuable when invariants matter more than example cases:

  • parser round trips
  • serialization stability
  • ordering guarantees

Snapshot testing is useful when output structure matters:

  • CLI output
  • generated config
  • structured serialization

Test doubles in Rust often come from traits rather than mocking frameworks first. If behavior is abstracted behind a trait, fake implementations are often enough.

Step 7 - Common Misconceptions

Wrong model 1: “The borrow checker means fewer tests are needed.”

Correction: memory safety and behavioral correctness are different.

Wrong model 2: “Integration tests are just slower unit tests.”

Correction: they validate a different contract: the public API as a consumer sees it.

Wrong model 3: “Doctests are cosmetic.”

Correction: they are executable examples and one of the best ways to stop docs from rotting.

Wrong model 4: “Mocking is always the right way to test.”

Correction: in Rust, small traits and real-value tests are often cleaner than heavy mocking.

Step 8 - Real-World Pattern

Mature Rust repositories often rely heavily on:

  • integration tests for CLI and HTTP behavior
  • snapshot tests for user-visible output
  • doctests for public libraries
  • properties for parsers and serializers

Tests are often the fastest map into an unfamiliar codebase because they show intended usage instead of implementation detail first.

Step 9 - Practice Block

Code Exercise

Write:

  • one unit test
  • one integration-test idea
  • one doctest example

for a small parser function.

Code Reading Drill

Explain what this test can access and why:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
}
}

Spot the Bug

Why is this test likely brittle?

#![allow(unused)]
fn main() {
assert_eq!(format!("{value:?}"), "State { x: 1, y: 2 }");
}

Refactoring Drill

Take a long integration test that mixes setup, action, and assertions chaotically. Restructure it into a clearer scenario.

Compiler Error Interpretation

If a doctest fails because an item is private, translate that as: “my documentation example is pretending to be a crate user, but I documented an internal-only path.”

Step 10 - Contribution Connection

After this chapter, you can read and add:

  • unit and integration tests
  • doctest examples
  • property and snapshot coverage
  • regression tests for reported bugs

Good first PRs include:

  • turning a bug report into a failing test
  • adding missing doctests to public APIs
  • improving snapshot coverage for CLI output

In Plain English

Rust catches many mistakes before the program runs, but it cannot tell whether your feature does the right thing. Tests and docs close that gap. That matters because good systems code is not just safe code; it is code whose behavior stays trustworthy over time.

What Invariant Is Rust Protecting Here?

Behavioral contracts, public examples, and regression boundaries must stay true even when internal implementations change.

If You Remember Only 3 Things

  • Unit, integration, and doctests serve different purposes.
  • Tests are often the best map into a codebase’s intended behavior.
  • The type system reduces a class of bugs; it does not remove the need for behavioral verification.

Memory Hook

Types are the building frame. Tests are the load test. The frame can be perfect and still fail if the wrong bridge is attached to it.

Flashcard Deck

QuestionAnswer
What is #[cfg(test)] for?Compiling test-only code when running the test harness.
What can unit tests access that integration tests usually cannot?Private items in the same module tree.
What do integration tests validate?The public API from an external consumer perspective.
Why are doctests valuable?They keep examples executable and documentation honest.
When is proptest useful?When invariants matter across many generated inputs.
When is insta useful?When structured output should remain stable and reviewable.
Why are bug-regression tests valuable?They prevent the same failure from quietly returning later.
Why can tests be a good onboarding tool?They show intended usage and edge cases clearly.

Chapter Cheat Sheet

NeedTest layerWhy
Pure local logicunit testfast and close to code
Public API workflowintegration testconsumer perspective
Executable docsdoctestexample correctness
Output stabilitysnapshot testvisible diff review
General invariantproperty testmany generated cases