Doc Tests
Doc Tests
Level 8 — Testing & Documentation Code examples in doc comments (
///) that are compiled and run as tests.
1. Prerequisites
- Comments — The
///syntax used to write documentation for functions.
2. Term Category
Rust Tooling (the documentation enforcer): In most languages, documentation is just raw text. If you write a code example in the comments showing how to use your function, and then 6 months later you change the function's arguments, your documentation is now broken and lying to your users!
Rust prevents this by turning your documentation examples into actual, runnable tests. When you run cargo test, Cargo extracts all the code blocks from your comments and executes them.
3. Explanation
(1) Design Motivation — "Why did we design this?"
The Rust designers wanted crates.io to be the best, most reliable package registry in the world. Good packages require good documentation, and good documentation absolutely requires code examples.
But code examples rot faster than anything else in a codebase. A developer updates the code but forgets to update the comments.
By automatically extracting all markdown code blocks out of /// comments and running them during cargo test, Rust guarantees that every single code example in your documentation actually compiles and works perfectly. If your code example is outdated, your build fails!
(2) Reality Metaphor
Imagine reading an instruction manual for a new Blender.
The manual says: "Press the RED button to blend." But the factory updated the blender 3 months ago, and now the button is BLUE. You press the red button, and the blender catches on fire. The manual lied to you!
A Doc Test is like a factory robot that reads the instruction manual every single night, presses the exact buttons the manual tells it to press on a real blender, and throws a massive alarm if the blender doesn't turn on.
(3) Rust Code Examples
Short Snippet (The Basic Doc Test)
You don't need #[test]. You literally just write a markdown code block inside a triple-slash comment!
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// let result = my_library::add(2, 2);
/// assert_eq!(result, 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
When you run cargo test, Cargo will say: Doc-tests my_library ... ok!
Fuller Example (Hiding Boilerplate)
Sometimes you need 10 lines of setup code (like connecting to a database) just to make a 2-line code example work. But you don't want the reader to see that ugly setup code in the documentation!
Rust allows you to prefix lines with # inside the code block. These lines are hidden from the reader in the final documentation, but the compiler still sees them and runs them!
/// Fetches the active user from the database.
///
/// ```
/// # // The reader will NOT see these hidden setup lines!
/// # let db = Database::connect_mock();
/// # db.insert_test_user("Alice");
/// #
/// // The reader WILL see this!
/// let user = my_library::get_user(&db);
/// assert_eq!(user.name, "Alice");
/// ```
pub fn get_user(db: &Database) -> User { ... }
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Doc Tests Scoping and Lifecycle Rules
The mistake: Assuming Doc Tests instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("doc_tests_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("doc_tests_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Doc Tests State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Doc Tests through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Doc Tests Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Doc Tests instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: Asynchronous Database Client Doc Tests with # Hidden Hashing & Connection Setup
Scenario: Problem Statement:
You are authoring library documentation for a high-performance database connection pool (AsyncDbClient). The documentation needs to demonstrate how users query database records using doc tests, while using # hidden lines to perform mock connection initialization and setup without cluttering the rendered documentation HTML.
Requirements: Requirements:
- Define
DbErrorenum withNotFoundandConnectionFailedvariants derivingDebugandPartialEq. - Struct
AsyncDbClientstoringurl: Stringand internal record cacheHashMap<String, String>. - Implement
AsyncDbClient::connect(url: &str) -> Result<Self, DbError>andquery_user_role(&self, username: &str) -> Result<String, DbError>. - Include a comprehensive
///doc comment onquery_user_rolecontaining runnable doc tests that hide connection setup boilerplate using#. - In
#[cfg(test)] mod tests, write unit tests verifying query successes, missing users, and assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq)]
pub enum DbError {
NotFound(String),
ConnectionFailed,
}
/// Client for interacting with remote database clusters.
pub struct AsyncDbClient {
url: String,
records: HashMap<String, String>,
}
impl AsyncDbClient {
/// Connects to a remote database cluster specified by `url`.
pub fn connect(url: &str) -> Result<Self, DbError> {
if url.is_empty() {
return Err(DbError::ConnectionFailed);
}
let mut records = HashMap::new();
records.insert("alice".to_string(), "admin".to_string());
records.insert("bob".to_string(), "user".to_string());
Ok(Self {
url: url.to_string(),
records,
})
}
/// Queries the user role for `username`.
///
/// # Examples
/// ```
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// # let client = AsyncDbClient::connect("postgres://localhost:5432/db")?;
/// let role = client.query_user_role("alice")?;
/// assert_eq!(role, "admin");
/// # Ok(())
/// # }
/// ```
pub fn query_user_role(&self, username: &str) -> Result<String, DbError> {
self.records
.get(username)
.cloned()
.ok_or_else(|| DbError::NotFound(username.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_db_client_queries() {
let client = AsyncDbClient::connect("postgres://localhost:5432/db").unwrap();
let role = client.query_user_role("alice");
assert!(role.is_ok());
assert_eq!(role.unwrap(), "admin");
let err = client.query_user_role("charlie");
assert!(matches!(err, Err(DbError::NotFound(_))));
}
}
Technical Explanation:
- Hidden Setup Lines (
#): Lines starting with#in doc test markdown blocks run duringcargo test --doc, but are omitted from HTML rendered bycargo doc. This keeps user-facing examples clean while ensuring code correctness. - Fallible Doc Tests: Returning
Result<(), Box<dyn Error>>inside hiddenmain()blocks allows using?operators directly inside documentation examples.
Exercise 2: should_panic Doc Tests for Bounded Buffer Invariants
Scenario: Problem Statement:
Doc tests can verify intentional failure modes. You are implementing a fixed-capacity circular buffer (BoundedBuffer<T>) that panics if initialized with zero capacity or when pushing beyond capacity. You must document these panicking behaviors using ```should_panic doc tests.
Requirements: Requirements:
- Struct
BoundedBuffer<T>holdingstorage: Vec<T>andcapacity: usize. - Implement
BoundedBuffer::new(capacity: usize) -> Self(panics with"Capacity must be non-zero"ifcapacity == 0). - Implement
push(&mut self, item: T)(panics with"Buffer overflow"ifstorage.len() >= capacity). - Write
///doc comments fornewandpushcontaining runnableshould_panicdoc tests. - In
#[cfg(test)] mod tests, write unit tests verifying non-panicking push/pop operations and assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
/// Fixed-capacity circular element buffer.
pub struct BoundedBuffer<T> {
storage: Vec<T>,
capacity: usize,
}
impl<T> BoundedBuffer<T> {
/// Creates a new [`BoundedBuffer`] with the specified capacity.
///
/// # Panics
/// Panics if `capacity` is 0.
///
/// # Examples
/// ```should_panic
/// # use std::panic;
/// // Panics because capacity is zero
/// let buffer: BoundedBuffer<i32> = BoundedBuffer::new(0);
/// ```
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "Capacity must be non-zero");
Self {
storage: Vec::with_capacity(capacity),
capacity,
}
}
/// Pushes an item into the buffer.
///
/// # Panics
/// Panics if the buffer is at capacity.
///
/// # Examples
/// ```should_panic
/// # let mut buffer = BoundedBuffer::new(1);
/// buffer.push(10);
/// buffer.push(20); // Panics due to overflow
/// ```
pub fn push(&mut self, item: T) {
assert!(self.storage.len() < self.capacity, "Buffer overflow");
self.storage.push(item);
}
/// Removes and returns the last element.
pub fn pop(&mut self) -> Option<T> {
self.storage.pop()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_buffer_operations() {
let mut buf = BoundedBuffer::new(2);
buf.push(100);
buf.push(200);
assert_eq!(buf.pop(), Some(200));
assert_eq!(buf.pop(), Some(100));
assert_eq!(buf.pop(), None);
}
}
Technical Explanation:
- Testing Panic Contracts with
should_panic: Addingshould_panicto a doc test code fence tellscargo test --docto expect a panic. If the example runs without panicking, the doc test fails. - Executable API Contract Verification:
should_panicdoc tests mathematically guarantee that documentation warnings regarding panics accurately reflect implementation behavior.
Exercise 3: Cross-Crate Intra-Doc Links & Complex Doc Test Verification
Scenario: Problem Statement:
When writing crate documentation, doc tests often demonstrate complex type interactions while relying on intra-doc links ([Token], [AuthEngine]) to connect related items.
Requirements: Requirements:
- Define struct
Tokenwithid: Stringandexpires_at: u64. - Define
AuthEnginewith methodvalidate(&self, token: &Token) -> bool. - Document
AuthEngine::validatewith doc comments containing intra-doc links and runnable doc tests demonstrating token validation. - In
#[cfg(test)] mod tests, write unit tests verifying expired vs active token checks with assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
/// Authentication security token.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token {
pub id: String,
pub expires_at: u64,
}
/// Engine for validating incoming authentication [`Token`] instances.
pub struct AuthEngine {
pub current_time: u64,
}
impl AuthEngine {
/// Constructs a new [`AuthEngine`] initialized to `current_time`.
pub fn new(current_time: u64) -> Self {
Self { current_time }
}
/// Validates whether a [`Token`] is valid and not expired.
///
/// # Examples
/// ```
/// let engine = AuthEngine::new(1000);
/// let token = Token { id: "tok_123".to_string(), expires_at: 2000 };
/// assert!(engine.validate(&token));
/// ```
pub fn validate(&self, token: &Token) -> bool {
!token.id.is_empty() && token.expires_at > self.current_time
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_validation() {
let engine = AuthEngine::new(1000);
let valid_token = Token { id: "valid_1".to_string(), expires_at: 1500 };
let expired_token = Token { id: "expired_1".to_string(), expires_at: 500 };
assert!(engine.validate(&valid_token));
assert!(!engine.validate(&expired_token));
assert_ne!(valid_token, expired_token);
}
}
Technical Explanation:
- Intra-Doc Hyperlinking: Branded bracket references (e.g.
[Token]) auto-link across HTML generated bycargo doc. - Doc Test Execution:
cargo test --doccompiles code examples as external consumers, catching missing imports or scope errors.
6. Related Terms
cargo doc— The command that actually generates the beautiful HTML website from these///comments.- Integration Tests — Like Integration Tests, Doc Tests can only test the
pubAPI of a Library crate.
7. Key Takeaways
- Any
```code block inside a///doc comment is automatically compiled and run as a test. - This mathematically guarantees your documentation is never outdated or lying to the user.
- You can hide ugly boilerplate setup code in your examples by prefixing the lines with
#. - Just like Integration Tests, Doc Tests only run for Library Crates (
src/lib.rs), not Binary Crates (main.rs).