PHASE 4 ← Back to Course
16 / 17

Testing and Best Practices

Write reliable code, unit tests, integration tests, documentation, and idiomatic Rust patterns.

1

Unit Tests (#[test], #[cfg(test)])

Unit tests verify individual components in isolation. They live alongside your code inside a #[cfg(test)] module and are compiled only when running cargo test.

Rust
// Basic unit test structure
fn add(a: i32, b: i32) -> i32 {
    a + b
fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
        assert_eq!(add(-1, 1), 0);
        assert_eq!(add(0, 0), 0);
    }
    #[test]
    fn test_divide_success() {
        assert_eq!(divide(10, 2).unwrap(), 5);
        assert_eq!(divide(7, 2).unwrap(), 3);
    }
    #[test]
    fn test_divide_by_zero() {
        assert!(divide(5, 0).is_err());
        match divide(10, 0) {
            Err(msg) => assert_eq!(msg, "Division by zero"),
            Ok(_) => panic!("Should have errored"),
        }
    }
    #[test]
    #[should_panic(expected = "attempt to subtract with overflow")]
    fn test_overflow() {
        let _ = i32::MIN - 1;
    }
}
Rust
// Testing with custom messages and assertions
struct Calculator;
impl Calculator {
    fn factorial(n: u32) -> u32 {
        match n {
            0 | 1 => 1,
            n => n * Self::factorial(n - 1),
        }
    }
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_factorial() {
        assert_eq!(Calculator::factorial(0), 1);
        assert_eq!(Calculator::factorial(1), 1);
        assert_eq!(Calculator::factorial(5), 120,
            "5! should equal 120");
        assert_eq!(Calculator::factorial(10), 3628800);
    }
    #[test]
    fn test_factorial_properties() {
        let n = 6;
        let result = Calculator::factorial(n);
        assert!(
            result > 0,
            "Factorial of {} should be positive, got {}",
            n, result
        );
    }
}
💡

Key Test Macros

assert! checks a boolean. assert_eq! and assert_ne! compare values with helpful diff output on failure. All accept an optional format string as the last arguments for custom error messages.

2

Integration Tests

Integration tests verify multiple components working together. They live in the tests/ directory and import your library as an external consumer would.

Rust
// Integration test file: tests/integration_test.rs
// Tests go in the tests/ directory
// They import the library like external code would
fn add_two(a: i32) -> i32 {
    a + 2
fn multiply_by_three(a: i32) -> i32 {
    a * 3
fn complex_operation(x: i32) -> i32 {
    multiply_by_three(add_two(x))
#[test]
fn integration_test_complex_operation() {
    // Test that multiple functions work together
    assert_eq!(complex_operation(3), 15);  // (3 + 2) * 3 = 15
    assert_eq!(complex_operation(0), 6);   // (0 + 2) * 3 = 6
    assert_eq!(complex_operation(-2), 0); // (-2 + 2) * 3 = 0
}

Unit vs Integration Tests

Unit tests live inside src/ in #[cfg(test)] modules and can test private functions. Integration tests live in tests/ and can only access your public API, just like a real consumer of your crate.

3

Doc Tests

Documentation examples that double as executable tests. Write them in /// doc comments using fenced code blocks, cargo test runs them automatically.

Rust
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// let result = add_docs(2, 3);
/// assert_eq!(result, 5);
/// ```
///
/// ```
/// assert_eq!(add_docs(-1, 1), 0);
/// ```
pub fn add_docs(a: i32, b: i32) -> i32 {
    a + b
/// Demonstrates error handling in doc tests.
///
/// # Examples
///
/// ```
/// let result = divide_docs(10, 2);
/// assert!(result.is_ok());
/// assert_eq!(result.unwrap(), 5);
/// ```
///
/// ```
/// let result = divide_docs(5, 0);
/// assert!(result.is_err());
/// ```
pub fn divide_docs(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(a / b)
    }
}
💡

Doc Tests Keep Examples Honest

Because doc tests are compiled and executed, your documentation examples never go stale. If you refactor an API, failing doc tests immediately flag outdated examples.

4

Test Organisation

Organise tests into modules that mirror your source structure. Each module can have its own #[cfg(test)] block with focused, well-named tests.

Rust
// src/lib.rs - organising tests in modules
pub mod math {
    pub fn add(a: i32, b: i32) -> i32 { a + b }
    pub fn subtract(a: i32, b: i32) -> i32 { a - b }
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_add() { assert_eq!(add(2, 2), 4); }
        #[test]
        fn test_subtract() { assert_eq!(subtract(5, 3), 2); }
    }
pub mod string_utils {
    pub fn reverse(s: &str) -> String {
        s.chars().rev().collect()
    }
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_reverse() {
            assert_eq!(reverse("hello"), "olleh");
            assert_eq!(reverse(""), "");
        }
    }
}
5

Benchmarking Basics

Measure performance with Criterion.rs. Benchmarks live in the benches/ directory and help you track regressions.

Rust
// Benchmarks typically go in benches/ directory
// [dev-dependencies]
// criterion = "0.5"
fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        n => fibonacci(n - 1) + fibonacci(n - 2),
    }
fn main() {
    // Quick local benchmark without criterion
    let start = std::time::Instant::now();
    for _ in 0..1000 {
        let _ = fibonacci(20);
    }
    let duration = start.elapsed();
    println!("Fibonacci(20) x1000: {:?}", duration);
}
💡

Use Criterion for Real Benchmarks

Criterion.rs provides statistical analysis, warm-up iterations, and comparison against previous runs. It generates HTML reports with charts, far more reliable than manual timing with Instant.

6

Clippy and rustfmt

Clippy is Rust's official linter that catches common mistakes and suggests idiomatic improvements. rustfmt enforces consistent formatting across your codebase.

Rust
// Clippy warnings and fixes
// BAD: unnecessary else block
fn bad_style(x: i32) -> i32 {
    if x > 0 {
        return x;
    } else {
        return -x;
    }
// GOOD: idiomatic Rust
fn good_style(x: i32) -> i32 {
    if x > 0 { x } else { -x }
// BAD: overly complex single-element Vec
fn inefficient() {
    let v: Vec<_> = std::iter::once(42).collect();
    println!("{:?}", v);
// GOOD: simple and direct
fn efficient() {
    let v = vec![42];
    println!("{:?}", v);
// Run: cargo clippy -- -D warnings
// Format: cargo fmt
// Check formatting: cargo fmt -- --check
⚠️

Add to CI

Run cargo clippy -- -D warnings and cargo fmt -- --check in your CI pipeline. This catches style issues and common bugs before they reach code review.

7

Project Structure Best Practices

A well-organised project layout makes navigation, testing, and collaboration easier. Follow Cargo's conventions.

Rust
// Recommended project structure:
//
// my_project/
// ├── Cargo.toml              # Project manifest
// ├── Cargo.lock              # Locked dependencies
// ├── src/
// │   ├── lib.rs             # Library root
// │   ├── main.rs            # Binary entry point
// │   ├── module1.rs         # Public modules
// │   └── subdir/
// │       └── module2.rs
// ├── tests/                  # Integration tests
// │   ├── integration_test.rs
// │   └── common.rs          # Shared test utilities
// ├── benches/               # Benchmarks
// │   └── my_bench.rs
// └── examples/              # Runnable examples
//     └── example1.rs
TOML
# Cargo.toml best practices
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
authors = ["Your Name"]
license = "MIT OR Apache-2.0"
description = "A brief description"
repository = "https://github.com/user/project"
keywords = ["keyword1", "keyword2"]
categories = ["command-line-utilities"]
8

Recommended Crates Ecosystem

The Rust ecosystem has excellent crates for nearly every use case. Here are the most popular and battle-tested options organised by category.

Rust
// Essential crates ecosystem
// Serialisation: serde, serde_json, toml, bincode
// Web frameworks: axum, actix-web, rocket, warp
// Async runtime: tokio, async-std
// Database: sqlx, diesel, rusqlite, mongodb
// CLI tools: clap, structopt, argh
// Error handling: anyhow, thiserror, color-eyre
// Logging: tracing, log, env_logger
// Testing: proptest, quickcheck, rstest
// Performance: rayon (parallelism), parking_lot (Mutex)
// Utilities: itertools, regex, uuid, chrono, rand
#[cfg(test)]
mod crate_examples {
    #[test]
    fn common_patterns() {
        // Using rayon for parallelism
        let nums: Vec<i32> = (1..100).collect();
        let sum: i32 = nums.iter().sum();
        assert!(sum > 0);
    }
}

Start with the Essentials

For any new project, consider starting with serde (serialisation), anyhow + thiserror (error handling), tracing (logging), and clap (CLI). These form a solid foundation for almost any Rust application.

← Previous Chapter 16 of 17 Next →