How to Write Examples for Your Rust Crate

Add a # Examples section in doc comments with a runnable code block to demonstrate function usage and verify correctness.

The problem with dry documentation

You publish a crate. Someone clones it, opens the documentation, and tries to copy a snippet. The snippet fails to compile because it is missing imports. Or worse, the example works in isolation but breaks when mixed with their actual code. Dry documentation pushes users away. Rust solves this by making your examples executable code.

Doc tests are just regular tests

Doc tests are not markdown decorations. They are actual test functions that the compiler extracts, wraps, and runs. Think of them as a contract between your code and the people who will use it. If the contract breaks, the build fails before anyone else sees it. The compiler treats your examples as first-class citizens. Write them like code, not prose.

The minimal setup

Every public function, struct, or module can carry a # Examples section inside its doc comment. The section must be followed by a fenced code block. The block runs as a standalone program when you execute cargo test.

/// Adds one to the provided integer.
///
/// # Examples
///
/// ```
/// // Import the function directly for the test scope
/// use my_crate::add_one;
///
/// // Call the function and verify the output
/// assert_eq!(6, add_one(5));
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

The /// syntax attaches the documentation to the item below it. The # Examples header tells rustdoc to render the block in the generated HTML. The triple backticks tell the test harness to compile and execute the contents. The assert_eq! macro proves the behavior matches expectations. If the assertion fails, the test fails. If the code does not compile, the test fails. Make your documentation runnable. If it does not compile, it does not exist.

What happens when you run cargo test

When you run cargo test, the build system splits your work into two phases. It compiles your library or binary first. Then it scans every doc comment for fenced code blocks. It extracts each block, wraps it in a fn main(), and compiles it as a separate crate. The test harness runs each generated binary sequentially. It captures standard output and standard error. It reports pass or fail for every example.

This extraction process explains why doc tests behave differently from unit tests. Unit tests live inside your crate. They share the same namespace and can access private items. Doc tests live outside your crate. They only see what you publish. They start from a blank slate. Import everything explicitly. The test harness does not inherit your module tree.

Real-world patterns and hidden lines

Real examples often require setup code that clutters the documentation. You might need to create a mock database, initialize a logger, or define a helper struct. The community convention is to hide that boilerplate while keeping it in the test. Prefix any line with # to strip it from the rendered docs but preserve it in the compiled test.

/// Builds a configuration from a set of key-value pairs.
///
/// # Examples
///
/// ```
/// // Hide the setup boilerplate from the rendered docs
/// # let mut builder = my_crate::ConfigBuilder::new();
/// # builder.set("timeout", "30s");
///
/// // Show only the relevant usage pattern
/// let config = builder.build();
/// assert!(config.is_valid());
/// ```
pub struct ConfigBuilder { /* fields */ }

The # prefix tells rustdoc to omit the line when generating HTML. The test harness still compiles it. This keeps the documentation clean while maintaining a complete, runnable example. You can also control execution behavior with block attributes. Add no_run when the example requires user interaction or takes longer than two seconds. Add should_panic when documenting error conditions and you want to prove the panic message matches expectations.

/// Attempts to parse a JSON payload.
///
/// # Examples
///
/// ```should_panic
/// // Hide the import to keep the example focused
/// # use my_crate::parse_json;
///
/// // Prove that invalid input triggers the expected panic
/// parse_json("{ broken json }");
/// ```
pub fn parse_json(input: &str) { /* implementation */ }

The should_panic attribute flips the success condition. The test passes only if the code panics. You can attach an expected message to verify the exact panic string. This pattern prevents silent regressions in error handling paths. Trust the test harness to enforce your error contracts.

Common traps and compiler messages

Doc tests fail for predictable reasons. The most common trap is assuming imports carry over from the module. They do not. If you forget to import a type, the compiler rejects you with E0432 (unresolved import). Add the use statement inside the doc test block.

Another frequent issue is writing a full program without a main function. The test harness wraps your block in fn main(), but if you define your own main inside the block, you get a duplicate definition error. Strip the fn main() from doc tests. Let the harness provide it.

You will also encounter E0601 (main function not found) if you accidentally use the ignore attribute on a block that should run. The ignore attribute tells the harness to skip compilation entirely. Use it only for examples that depend on external hardware, network calls, or third-party services that are unavailable in CI.

/// Connects to the external weather API.
///
/// # Examples
///
/// ```ignore
/// // Skip compilation because this requires a live network connection
/// let client = my_crate::WeatherClient::new();
/// let forecast = client.get_forecast("London");
/// ```
pub struct WeatherClient { /* fields */ }

The ignore attribute is a safety valve. It prevents CI pipelines from failing on flaky external dependencies. The community convention is to keep ignore blocks to a minimum. Prefer no_run when the code is valid but slow. Reserve ignore for truly untestable scenarios. Import everything explicitly. The test harness starts from a blank slate.

When to use doc tests versus other approaches

Rust provides multiple testing layers. Each layer serves a different verification goal. Pick the right tool for the job.

Use doc tests for public API surface verification. Use unit tests with the #[test] attribute for internal logic and edge cases. Use integration tests in the tests/ directory for cross-module workflows and real-world scenarios. Reach for no_run when the example requires user interaction or takes longer than two seconds. Pick should_panic when documenting error conditions and you want to prove the panic message matches expectations.

Doc tests prove that your public interface works exactly as advertised. They run automatically when someone builds your crate. They double as living documentation. Unit tests prove that your internal algorithms handle boundary conditions. They run faster and can access private helpers. Integration tests prove that your crate plays nicely with other crates and real file systems. They run slower but catch architectural mismatches. Match the test type to the verification goal. Keep the API surface honest.

Where to go next