When documentation lies
You publish a library. A developer copies the example from your docs, pastes it into their project, and it fails. They open an issue. You check the source code; the function works perfectly. The problem is the example. You updated the function three months ago but forgot to update the comment. The docs are stale. The example is a lie.
This happens in every language. Documentation drifts from code. Examples become text artifacts that nobody runs. Rust changes the contract. In Rust, examples are code. If the example doesn't compile or doesn't match the behavior, the build fails. Your documentation cannot lie because the compiler enforces the truth.
Doc tests turn your /// comments into executable tests. rustdoc extracts the code blocks, compiles them, runs them, and checks the results. If the test passes, your docs are accurate. If it fails, you fix the docs or the code before anyone else sees the error.
Doc tests as executable contracts
Think of a doc test like a recipe card that also cooks the meal. In Python or JavaScript, you might write a code block in a docstring or a markdown file. It's text. You hope it works. You might run it manually once. Rust treats that block as a first-class citizen. The toolchain reads the comment, wraps the code in a test harness, and executes it alongside your unit tests.
The name comes from rustdoc, the tool that generates HTML documentation. rustdoc does two jobs. It renders your comments into a website. It also runs the code blocks as tests. When you run cargo test, Cargo invokes rustdoc to check every doc test in your crate. The output shows doc my_crate::function ... ok for each passing example.
This creates a feedback loop. You write the example to show how to use the API. The test verifies the example works. If you refactor the function and break the example, the test catches it immediately. The docs stay synchronized with the implementation.
The minimal doc test
A doc test lives in the triple-slash comment above a public item. You mark the code block with a # Examples header. The code block must be a valid Rust snippet that compiles and runs. Use assert_eq! to verify the result.
/// Adds two integers and returns the sum.
///
/// # Examples
///
/// ```
/// use my_crate::add; // Import the function so the test can find it.
///
/// let result = add(2, 3);
/// assert_eq!(result, 5); // Verify the output matches the claim.
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Run the test with cargo test. The output confirms the doc test passed.
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
The test runs in isolation. It doesn't share state with other tests. It compiles as a separate unit. This isolation prevents side effects from leaking between examples.
How rustdoc runs your docs
When you execute cargo test, the compiler scans your source files for doc comments containing code blocks. For each block, rustdoc generates a temporary test file. It injects the code from the comment. It adds a main function wrapper. It compiles the file. It links it against your crate. It runs the binary.
If the code panics or an assertion fails, the test reports an error. The error message points to the doc test, not a separate test file. This makes debugging straightforward. You see the failing example in the output. You fix the comment or the code.
Doc tests run after unit tests. They are slower because each test compiles a separate binary. The compiler has to parse the code block, resolve imports, and link the crate. For a small crate, the overhead is negligible. For a large crate with many doc tests, the compile time adds up. Use doc tests for public API examples, not for exhaustive coverage.
Hiding the boilerplate
Real-world examples often require setup code. Imports, struct construction, or mock data can clutter the example. You want the docs to show the essential usage, not the plumbing. Rustdoc lets you hide lines in the rendered output while keeping them in the test.
Prefix a line with # inside the code block. The line runs during the test but disappears from the generated HTML. This keeps the example clean for readers while ensuring the code compiles.
/// Parses a configuration string into a settings object.
///
/// # Examples
///
/// ```
/// use my_crate::Config; // Hide this import in the rendered docs.
///
/// let config = Config::parse("debug=true");
/// assert!(config.debug);
/// ```
pub struct Config {
pub debug: bool,
}
impl Config {
pub fn parse(s: &str) -> Self {
// Implementation details...
Config { debug: true }
}
}
Convention aside: Use # only for setup code the user doesn't need to see. Don't hide the core logic. If the user copies the visible code, it should still make sense. Hiding imports is standard. Hiding the function call is not.
The # syntax applies to any line. You can hide imports, variable declarations, or helper functions. You can even hide the use statement for the crate itself. This is common when the example uses internal types.
/// ```
/// use my_crate::internal::SecretType; // Hide internal types from docs.
///
/// let secret = SecretType::new();
/// assert!(secret.is_valid());
/// ```
Treat hidden lines as implementation details of the test. If the hidden code breaks, the test fails. The docs still render, but the build is red. Fix the hidden code to restore the green build.
The output trap
Doc tests capture standard output. If your example prints text, the test checks that the output matches exactly. This catches subtle bugs where the output changes unexpectedly. It also creates a trap for beginners.
If you add a println! to debug an example, the test now expects that output. If you remove the print later, the test fails because the output no longer matches. The error message shows the expected output versus the actual output.
thread 'my_crate::add' panicked at 'output mismatch', src/lib.rs:1:1
expected:
5
actual:
The fix is to remove the println! or update the expected output. Doc tests are strict about output. If you don't want to check the output, avoid printing. Use assertions instead. Assertions verify values without producing output.
If you must show output in the docs, wrap the block with output checking. Rustdoc compares the text after the code block with the program's stdout. This is useful for demonstrating CLI tools or formatting functions.
/// Prints a greeting.
///
/// # Examples
///
/// ```
/// use my_crate::greet;
///
/// greet("Alice");
/// ```
///
/// Output:
/// ```text
/// Hello, Alice!
/// ```
pub fn greet(name: &str) {
println!("Hello, {name}!");
}
The Output: section tells rustdoc to check the stdout. If the function changes the greeting format, the test fails. This keeps the output documentation accurate.
Don't fight the output check. Use it to verify that your examples produce the expected results. If the output is irrelevant, remove the print. Assertions are safer and faster.
Testing panics and errors
Doc tests can verify that code panics. Use the should_panic attribute on the code block. The test passes if the code panics. It fails if the code completes successfully. This is useful for documenting error conditions.
/// Divides two numbers. Panics if the divisor is zero.
///
/// # Examples
///
/// ```should_panic
/// use my_crate::divide;
///
/// divide(10, 0); // This should panic.
/// ```
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero");
}
a / b
}
You can also check the panic message. Add expected = "message" to the attribute. The test verifies the panic contains the text.
/// ```should_panic(expected = "Division by zero")
/// use my_crate::divide;
///
/// divide(10, 0);
/// ```
This ensures the panic message stays consistent. If you change the error text, the test fails. Update the attribute to match the new message.
Convention aside: Prefer returning Result over panicking. Doc tests for should_panic are less common in modern Rust. Use them only when panics are part of the API contract. For Result types, show the error case in the example.
/// ```
/// use my_crate::divide_safe;
///
/// assert_eq!(divide_safe(10, 0), Err("Division by zero"));
/// ```
This approach is safer and more idiomatic. It avoids panics and lets the caller handle errors.
Pitfalls and compiler errors
Doc tests run in a separate compilation unit. They don't inherit the module's imports or visibility. You must import everything explicitly. If you reference a function without importing it, the compiler rejects the doc test with E0425 (cannot find value in this scope).
Add use crate::function_name; inside the code block. For external crates, use use external_crate::item;. This rule applies to all items: functions, structs, enums, and constants.
Doc tests also can't access private items by default. If you try to use a private function in a doc test, you get E0603 (function is private). You have two options. Make the item public. Or use #[doc(hidden)] to keep it public but hide it from the docs. The doc test can still access it.
Another pitfall is the ignore attribute. You can mark a doc test with ignore to skip it. The test doesn't run. This is useful for examples that require external dependencies or manual setup. However, ignore is a debt signal. It means your docs contain code that doesn't work. Fix the example or remove it. Don't leave ignored tests in production code.
Use no_run when the code is valid but shouldn't execute. This is common for examples that print output, modify files, or take too long. The code compiles, proving it's correct. The test skips execution.
/// ```no_run
/// // This code compiles but won't run.
/// // Useful for examples that print output or require network access.
/// println!("Hello");
/// ```
Treat ignore as a TODO. Treat no_run as a deliberate choice. If you use ignore, add a comment explaining why. Future you will thank you.
Decision matrix
Use doc tests for public API examples. They show users how to call your function and verify the docs stay accurate.
Use #[cfg(test)] modules for internal logic, edge cases, and performance checks. Doc tests are too slow and verbose for exhaustive coverage.
Use no_run when the example demonstrates valid code that shouldn't execute, like a script that prints output or modifies files.
Use ignore only when the example requires external dependencies or manual setup that can't be automated. Treat ignore as a red flag. If a doc test is ignored, your docs are lying until you fix it.
Use should_panic when the API contract includes panics. Prefer Result types for error handling.
Use hidden lines with # to clean up boilerplate. Keep the visible code copy-pasteable and focused on the core usage.