19 / 20 · Day 7
Day 7 · Concept 19

Testing & benchmarks

Tests live next to the code in a #[cfg(test)] module. cargo test runs everything — unit tests, integration tests, doc tests. For benchmarks, criterion is the standard.


1 · Unit tests — colocated

rust src/lib.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn adds_positive() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn adds_negative() {
        assert_eq!(add(-1, -2), -3);
    }

    #[test]
    #[should_panic(expected = "divide by zero")]
    fn divide_by_zero_panics() {
        panic!("divide by zero");
    }
}

Same file. The #[cfg(test)] module is only compiled when running tests, so it adds no overhead to your binary.

2 · Integration tests — top-level tests/

rust tests/integration.rs
use mycrate::add;

#[test]
fn external_api_works() {
    assert_eq!(add(2, 2), 4);
}

Files in tests/ at the project root are compiled as separate crates that depend on the library. Use them for end-to-end tests that exercise the public API only.

3 · Doc tests — examples that run

rust src/lib.rs · examples in rustdoc
/// Adds two integers.
///
/// # Examples
/// ```
/// use mycrate::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

cargo test runs the code in your doc comments. Examples can't drift from reality — if they break, CI breaks. This single feature kills most stale-docs problems.

4 · Async tests

rust src/main.rs
#[tokio::test]
async fn fetches_data() {
    let result = fetch().await;
    assert_eq!(result, "ok");
}

async fn fetch() -> &'static str { "ok" }

Use #[tokio::test] instead of #[test]. It wires up a single-threaded runtime per test.

5 · Benchmarks — criterion

rust benches/my_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn fib(n: u64) -> u64 {
    if n < 2 { n } else { fib(n - 1) + fib(n - 2) }
}

fn bench(c: &mut Criterion) {
    c.bench_function("fib 20", |b| b.iter(|| fib(black_box(20))));
}

criterion_group!(benches, bench);
criterion_main!(benches);

black_box hides the value from the optimiser so the work isn't elided. cargo bench runs it; results report mean, std-dev, and a comparison against the previous run.

6 · Common mistakes

  • Forgetting cfg(test). Without it the test module compiles into release builds.
  • Sharing state across tests. Tests run in parallel by default. Use --test-threads=1 only as a workaround; usually means a missing dependency injection.
  • Asserting on Debug output — fragile across versions. Use assert_eq! on structured values.

7 · When it clicks

  • You write tests in the same file as the code by reflex.
  • Public API examples in doc comments — knowing they're checked.
  • Benchmarks before optimising, not after.
Found this useful?