Skip to Content
Course content

211: Doc Tests as Living Documentation

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you've just bought a complex piece of furniture from IKEA. You open the manual, and it tells you: "Insert Screw A into Slot B." You try it, but Slot B doesn't exist anymore because the manufacturer updated the design six months ago, but forgot to update the printed manual. You're left staring at a pile of wood and a useless piece of paper. That's exactly what happens in software when our documentation "rots"—the code evolves, but the examples in the README or the API docs stay frozen in time.

Rust solves this with doc tests. Think of doc tests as a little robot that lives inside your manual. Every time you run your tests, this robot reads your documentation, finds the code examples, and actually tries to build the furniture. If the robot fails to put Screw A into Slot B, the test fails. You're forced to either fix the code or update the manual. The documentation is no longer a static promise; it's a living part of your test suite.

Stop Lying to Your Users

I can't tell you how many times I've pulled my hair out because a library's documentation showed a function call that wouldn't even compile. In Rust, we use the /// (three slashes) syntax for documentation comments. Anything inside a markdown code block within those comments is treated as a test case by cargo test.

Let's look at a real example. Suppose we're building a simple DiscountCalculator. Instead of just telling the user it works, we show them—and we make the compiler prove it.

/// Calculates the final price after applying a percentage discount.
///
/// # Examples
///
/// ```
/// use my_store::DiscountCalculator;
/// 
/// let final_price = DiscountCalculator::apply(100.0, 20.0);
/// assert_eq!(final_price, 80.0);
/// ```
///
/// This function assumes the discount is a percentage (0-100).
pub struct DiscountCalculator;

impl DiscountCalculator {
    pub fn apply(price: f64, discount: f64) -> f64 {
        price * (1.0 - (discount / 100.0))
    }
}

When you run cargo test, Rust extracts that code block, wraps it in a hidden main function, and executes it. If you later change apply to take a decimal (0.20) instead of a percentage (20.0), the doc test will crash, alerting you that your documentation is now lying to your users.

Dealing with the "Happy Path" and Beyond

Most people only write doc tests for the "happy path," but I want you to push further. What happens when the input is garbage? You can document your error handling and test it simultaneously. If your function panics under certain conditions, you can tell the doc test to expect that using a special attribute.

Check this out:

/// Divides the total cost across a number of people.
///
/// # Panics
///
/// Panics if the number of people is zero.
///
/// ```
/// use my_store::BillSplitter;
/// 
/// // This should work fine
/// assert_eq!(BillSplitter::split(100.0, 4), 25.0);
/// 
/// // This will panic, and we tell Rust to expect it
/// #[should_panic]
/// BillSplitter::split(100.0, 0);
/// ```
pub struct BillSplitter;

impl BillSplitter {
    pub fn split(amount: f64, people: u32) -> f64 {
        if people == 0 {
            panic!("Cannot split a bill among zero people!");
        }
        amount / people as f64
    }
}

By adding #[should_panic] inside the doc test, you're essentially saying: "I'm documenting that this specific edge case causes a crash, and I want to make sure it continues to crash this way." It's a powerful way to ensure your safety guarantees don't accidentally disappear during a refactor.

Hidden Machinery and Customizing the Test

Sometimes, your doc test needs a bit of setup that you don't actually want the user to see in the final documentation—maybe some tedious imports or a helper function. You can hide these lines by starting them with a hash #. Rust will execute them, but rustdoc will hide them from the rendered HTML.

I use this constantly to keep examples clean. If I have a 10-line setup just to get a single variable ready, I'll hide the boilerplate so the reader can focus on the actual API call. It keeps the "signal-to-noise" ratio high while keeping the test fully reproducible.




📋 Practical Task

Implementing a Documented Password Validator

Your task is to create a PasswordValidator struct that ensures a password is at least 8 characters long and contains at least one digit. Instead of writing a separate test module, you will implement the requirements using Doc Tests.

  • Create a struct PasswordValidator with a method is_valid(password: &str) -> bool.
  • Write a documentation comment (///) for is_valid.
  • Inside that comment, include a # Examples section with at least three code blocks:
    1. A valid password that returns true.
    2. A password that is too short and returns false.
    3. A password with no digits that returns false.
  • Run cargo test to ensure your documentation is actually correct and that the "living documentation" passes.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.