How to use doc tests

Run mdbook test with the correct library path to execute doc tests in your Rust book.

When documentation lies

You publish a crate. Users adopt it. Six months later, you refactor a core function to handle a new edge case. You update the implementation. You forget to update the example in the doc comment. The example still compiles, but it demonstrates the old behavior. Or worse, the example breaks silently because the API changed in a way the test didn't catch.

Users copy the example from your docs. It fails. They open an issue. Trust evaporates.

Rust prevents this by making documentation executable. Doc tests turn your documentation examples into automated checks. If the code changes, the docs break. If the docs break, the build fails. You catch the rot before anyone else does.

The executable contract

A doc test is a code block inside a doc comment. You write it exactly like you would write an example for a user, using triple backticks. When you run cargo test, the compiler extracts these blocks, compiles them, and runs them as tests.

Doc tests serve two purposes. They verify that your public API works as described. They provide copy-pasteable examples that users can trust. If the example runs, the documentation is accurate.

The compiler treats doc tests as if they are written by an external user. This creates a strict boundary. Doc tests run in a separate crate context. They can only see public items. Private helpers, internal structs, and pub(crate) functions are invisible. This forces you to design a clean public interface. If you can't test something in a doc test, it probably shouldn't be part of the public API.

Minimal example

Start with a function and a doc comment containing a code block. The block must use triple backticks. The compiler recognizes the rust language tag implicitly inside doc comments.

/// Adds two integers and returns the sum.
///
/// ```
/// // Import the function (implicit in doc tests via `use crate::...`)
/// assert_eq!(add(2, 2), 4);
/// assert_eq!(add(-1, 1), 0);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Run cargo test. The output shows a test named after the function and the line number of the doc test.

running 1 test
test src/lib.rs - add (line 4) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

The test passes. The documentation matches the implementation.

What the compiler actually does

Doc tests are not magic. The compiler generates hidden test functions for every code block it finds. Here is the transformation.

The compiler takes the code inside the backticks. It wraps it in a fn main() function. It injects a use statement that brings the item being documented into scope. It compiles this wrapper as a separate crate. It links it against your library. It runs the resulting binary.

This separate crate model explains why doc tests behave differently from unit tests. Unit tests live inside your crate. They have access to everything, including private items. Doc tests live outside. They simulate a user importing your crate.

This separation is a feature. It catches API design flaws. If your doc test relies on a private helper, the test fails with E0425 (cannot find value in this scope). The error tells you that your example assumes access to internal details. Users won't have that access. You must either expose the helper or rewrite the example to use only public APIs.

Realistic usage with attributes

Real code needs more than basic assertions. Doc tests support attributes that control compilation and execution. You add attributes after the opening backticks.

Skipping execution

Use no_run when the code is valid but you don't want to execute it. This is common for examples that perform I/O, make network requests, or take a long time. The compiler still checks syntax and types. It just doesn't run the binary.

/// Fetches data from an external API.
///
/// This example shows the correct usage but is marked `no_run`
/// because it requires a network connection and an API key.
///
/// ```no_run
/// let client = ApiClient::new("https://api.example.com");
/// let data = client.fetch("/users").await;
/// println!("Got {} users", data.len());
/// ```
pub struct ApiClient {
    // Implementation details
}

Use ignore when you want to skip the test entirely. The compiler doesn't even compile the code. You must provide a reason. The convention is to add a comment explaining why the test is ignored. Overusing ignore defeats the purpose of doc tests. If a test is ignored, the documentation is no longer verified.

/// Calculates a complex simulation.
///
/// ```ignore
/// // Ignore: This simulation takes 45 seconds to run.
/// // Run manually with `cargo run --example simulation`
/// let result = run_simulation(10000);
/// assert!(result.is_valid());
/// ```
pub fn run_simulation(steps: usize) -> SimulationResult {
    // Heavy computation
    todo!()
}

Expecting failure

Use compile_fail when you want to show that invalid usage is rejected by the compiler. This is powerful for documenting error cases. The test passes only if the code fails to compile.

/// Creates a new user with a unique ID.
///
/// Panics if the ID is already taken.
///
/// ```compile_fail
/// // This should fail because the ID is duplicated.
/// // The compiler catches this at compile time due to const checks.
/// let user = User::new(0);
/// let duplicate = User::new(0);
/// ```
pub struct User {
    id: u32,
}

Use should_panic when you want to document runtime panics. The test passes if the code panics. You can optionally specify the expected panic message.

/// Divides two numbers.
///
/// Panics if the divisor is zero.
///
/// ```should_panic(expected = "division by zero")
/// let result = divide(10, 0);
/// ```
pub fn divide(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        panic!("division by zero");
    }
    a / b
}

Pitfalls and compiler errors

Doc tests have specific failure modes. Understanding these saves debugging time.

Private items are invisible

The most common error is trying to use a private function in a doc test. The compiler rejects this with E0425 (cannot find value in this scope) or E0603 (function is private).

/// Helper function for internal calculations.
///
/// ```
/// // This will fail. `helper` is private.
/// let x = helper(5);
/// ```
fn helper(n: i32) -> i32 {
    n * 2
}

The fix is to make the item public or remove the doc test. Doc tests are for public APIs. If you need to test private logic, use unit tests. Unit tests live inside the module and have full access.

Missing dependencies

Doc tests run in a separate crate. They don't automatically inherit dependencies from your Cargo.toml. If your example uses an external crate, you must import it explicitly.

/// Parses JSON data.
///
/// ```
/// use serde_json; // Explicit import required
/// let data = serde_json::from_str(r#"{"key": "value"}"#).unwrap();
/// ```
pub fn parse_json(input: &str) -> serde_json::Value {
    serde_json::from_str(input).unwrap()
}

If you forget the import, you get E0433 (failed to resolve). The error looks like a missing dependency, but it's usually just a missing use statement in the doc test.

The ignore trap

Teams sometimes mark failing doc tests as ignore to get the build green. This is a debt trap. Ignored tests provide zero value. They rot silently. If a test is ignored, the documentation is unverified. Users might copy broken code.

Treat ignored tests as a technical debt item. Fix the test or remove the example. A missing example is better than a broken one.

Decision matrix

Choose the right testing strategy based on what you are verifying.

Use doc tests for public API usage examples. They verify that the interface works as documented and provide trusted snippets for users.

Use unit tests for private helper functions and internal invariants. Unit tests live inside the module and can access private items. They are faster to run and better suited for edge cases that don't need user-facing examples.

Use no_run when the example demonstrates correct syntax but performs side effects you want to skip. This includes I/O, network calls, or long-running computations. The compiler still validates the code.

Use compile_fail when you need to show that invalid usage is rejected by the compiler. This documents error boundaries and helps users avoid common mistakes.

Use ignore only when the test requires external resources or takes longer than a few seconds, and always document the reason. Never use ignore to hide failing tests.

Use integration tests for cross-module workflows and end-to-end scenarios. Integration tests live in the tests/ directory and treat your crate as an external dependency. They verify that multiple public APIs work together correctly.

Where to go next

Doc tests are the first line of defense against rotting documentation. They keep your examples honest. They force you to design a clean public API. They give users code they can trust.

Write doc tests for every public function. Treat them as the canonical usage guide. If it's not in a doc test, users won't find it.