Make Docs Executable: Doc Tests
What if wrong documentation broke your build? Docs rot: the example in your README drifts from the real API, and nobody notices until a user copies it and files a confused bug report. Doc tests flip the incentive — the examples in your comments and README compile and run as part of the test suite, so stale documentation fails CI instead of failing your users. Your docs cannot lie, because they have to run.
How Rust doc tests work
Every fenced code block inside a /// doc comment is a test. cargo test runs them alongside your unit tests; cargo test --doc runs only the doc tests.
/// Adds two numbers.
///
/// ```
/// let sum = mycrate::add(2, 3);
/// assert_eq!(sum, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
Rustdoc preprocesses each block so short examples stay short: it wraps the code in fn main() if you didn't write one and injects extern crate mycrate; for you. Rename add later and this block stops compiling — the build goes red before the docs go wrong.
Lines starting with # compile but don't render, so readers see 2 lines of signal while the compiler sees the full setup:
/// ```
/// # fn setup() -> mycrate::Client { mycrate::Client::new() }
/// let client = setup();
/// client.ping();
/// ```
Not every example should run the same way
Some examples hit the network, some are deliberately broken, some must crash. Rustdoc gives each block an attribute instead of an excuse:
| Attribute | What it does | Use when |
|---|---|---|
| (none) | Compile and run | The default — prefer it |
no_run | Compile, don't execute | Network calls, side effects |
should_panic | Must panic to pass | Documenting failure modes |
compile_fail | Must fail to compile | Showing what the type system rejects |
ignore | Skip entirely | Last resort — it proves nothing |
compile_fail is the underrated one: it lets you document "this misuse won't compile" and have CI verify that the misuse still doesn't compile.
Test the README too
The README is the doc that drifts fastest, because it lives outside the compiler's sight. Pull it in:
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;
Every Rust code block in your README is now a doc test. The #[cfg(doctest)] gate keeps the struct out of your public API and rendered docs.
Same idea outside Rust
The principle is language-agnostic — executable examples predate Rust by decades:
- Python
doctestruns the>>>sessions in your docstrings:python -m doctest -v example.py, ordoctest.testfile("README.txt")for prose files. - pytest collects them suite-wide:
pytest --doctest-modulesfor docstrings,--doctest-glob="*.md"to sweep your Markdown docs into the same run. - No doc-test runner in your stack? Extract fenced blocks from your docs in CI and compile them as a script. The mechanism matters less than the property: examples that don't run are claims, not documentation.
Power-user moves
- Write the doc test first. For a public API it's a better TDD driver than a unit test: you're forced to design the call site a stranger will actually type.
- Use
?cleanly. Hide theResultplumbing so examples show error handling without boilerplate: end the block with a hidden# Ok::<(), io::Error>(()). - Doc tests are AI-agent fuel. Coding agents read your docs to learn your API — and they inherit every stale example as a hallucination seed. A doc-tested crate gives agents examples that are provably current, and gives you a red build the moment an agent's refactor breaks a documented contract.
- Budget the runtime. Each Rust doc test historically compiled as its own crate; the 2024 edition merges compatible doc tests to cut that cost (opt out per-block with
standalone_crate).
Resources
- rustdoc: Documentation tests — attributes, hidden lines, preprocessing rules
- rustdoc: the README
include_str!pattern - Python
doctestmodule —testmod,testfile, option flags - pytest: How to run doctests —
--doctest-modules,--doctest-glob
Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.