๐Ÿฆ€ Functional Rust

744: Unit Test Organisation: Modules, Helpers, AAA Pattern

Difficulty: 2 Level: Intermediate Tests live in the same file as production code, gated by `#[cfg(test)]`. No external framework required.

The Problem This Solves

In many languages, testing requires setting up a test runner, installing a framework, and organizing test files separately from production code. In Python you configure pytest; in JavaScript you wire up Jest. This overhead means beginners often skip testing, and even experienced developers delay writing tests for utility functions. Rust ships its own test runner โ€” no dependencies needed. You annotate a function with `#[test]` and run `cargo test`. Tests compile only in test mode (`#[cfg(test)]`), so they have zero impact on your release binary. The standard macros `assert_eq!`, `assert!`, and `#[should_panic]` cover almost everything. The bigger challenge is keeping tests readable as a codebase grows. The Arrange-Act-Assert pattern (AAA) and shared helper modules solve this. Helpers like `assert_approx_eq` and `assert_sorted` live in a `helpers` sub-module under `#[cfg(test)]`, so they're available to all tests but stripped from production builds.

The Intuition

Think of Python's `unittest.TestCase` โ€” but without the class boilerplate. In Rust, a test is just a free function annotated with `#[test]`. Tests in the same module can see private functions; tests in a `mod tests { use super::*; }` block see the public API. The `#[cfg(test)]` attribute is Rust's conditional compilation: that whole module only exists when you run `cargo test`. It's like `if __name__ == "__main__"` in Python, but enforced at the type level. Tests run in parallel by default. If a test panics, it fails. `assert_eq!(a, b)` panics with a helpful diff message when `a != b`.

How It Works in Rust

// Production code โ€” publicly accessible
pub fn clamp(lo: i32, hi: i32, x: i32) -> i32 {
 x.max(lo).min(hi)
}

// Test helpers โ€” only compiled during `cargo test`
#[cfg(test)]
mod helpers {
 pub fn assert_approx_eq(a: f64, b: f64, eps: f64) {
     assert!((a - b).abs() < eps,
         "assert_approx_eq: |{} - {}| >= {}", a, b, eps);
 }
}

// Unit tests โ€” access production code via `use super::*`
#[cfg(test)]
mod tests {
 use super::*;
 use helpers::*;

 // Arrange-Act-Assert pattern with a descriptive name
 #[test]
 fn test_clamp_when_below_lo_returns_lo() {
     let (lo, hi, x) = (0, 10, -5);      // Arrange
     let result = clamp(lo, hi, x);       // Act
     assert_eq!(result, 0);               // Assert
 }

 // For expected panics โ€” specify the panic message substring
 #[test]
 #[should_panic(expected = "divide by zero")]
 fn test_integer_division_by_zero_panics() {
     let _ = 5u32 / 0;
 }
}
Key points:

What This Unlocks

Key Differences

ConceptOCamlRust
Test declaration`let () = Alcotest.run ...` or ppx annotation`#[test]` attribute โ€” built in
Test isolationSeparate `_test.ml` files by convention`#[cfg(test)] mod tests` in same file
Assertion`Alcotest.(check int) "msg" expected actual``assert_eq!(actual, expected)`
Expected panicManual `try`/exception match`#[should_panic(expected = "...")]`
Parallel executionSequential by defaultParallel by default
Test helpersRegular functions in test file`#[cfg(test)] mod helpers` โ€” stripped from release
/// 744: Unit Test Organisation โ€” modules, helpers, AAA pattern

// โ”€โ”€ Code under test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

pub fn clamp(lo: i32, hi: i32, x: i32) -> i32 {
    x.max(lo).min(hi)
}

pub fn divide_checked(a: i64, b: i64) -> Option<i64> {
    if b == 0 { None } else { Some(a / b) }
}

pub fn is_palindrome(s: &str) -> bool {
    let bytes = s.as_bytes();
    let n = bytes.len();
    (0..n / 2).all(|i| bytes[i] == bytes[n - 1 - i])
}

pub fn fizzbuzz(n: u32) -> String {
    match (n % 3, n % 5) {
        (0, 0) => "FizzBuzz".into(),
        (0, _) => "Fizz".into(),
        (_, 0) => "Buzz".into(),
        _      => n.to_string(),
    }
}


// โ”€โ”€ Test helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[cfg(test)]
mod helpers {
    /// Assert that two f64 values are equal within epsilon.
    pub fn assert_approx_eq(a: f64, b: f64, eps: f64) {
        assert!((a - b).abs() < eps,
            "assert_approx_eq failed: |{} - {}| = {} >= {}",
            a, b, (a - b).abs(), eps);
    }

    /// Assert that a slice is sorted ascending.
    pub fn assert_sorted<T: Ord + std::fmt::Debug>(v: &[T]) {
        for w in v.windows(2) {
            assert!(w[0] <= w[1], "not sorted: {:?}", v);
        }
    }
}

// โ”€โ”€ Unit tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

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

    // โ”€โ”€ clamp โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    fn test_clamp_when_below_lo_returns_lo() {
        // Arrange
        let (lo, hi, x) = (0, 10, -5);
        // Act
        let result = clamp(lo, hi, x);
        // Assert
        assert_eq!(result, 0);
    }

    #[test]
    fn test_clamp_when_within_range_returns_x() {
        assert_eq!(clamp(0, 10, 5), 5);
    }

    #[test]
    fn test_clamp_when_above_hi_returns_hi() {
        assert_eq!(clamp(0, 10, 15), 10);
    }

    #[test]
    fn test_clamp_at_boundaries() {
        assert_eq!(clamp(0, 10, 0), 0);
        assert_eq!(clamp(0, 10, 10), 10);
    }

    // โ”€โ”€ divide_checked โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    fn test_divide_checked_non_zero_returns_some() {
        assert_eq!(divide_checked(10, 3), Some(3));
    }

    #[test]
    fn test_divide_checked_by_zero_returns_none() {
        assert_eq!(divide_checked(42, 0), None);
    }

    #[test]
    fn test_divide_checked_negative_dividend() {
        assert_eq!(divide_checked(-10, 2), Some(-5));
    }

    // โ”€โ”€ is_palindrome โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    fn test_palindrome_empty_is_palindrome() {
        assert!(is_palindrome(""));
    }

    #[test]
    fn test_palindrome_single_char_is_palindrome() {
        assert!(is_palindrome("a"));
    }

    #[test]
    fn test_palindrome_racecar_is_palindrome() {
        assert!(is_palindrome("racecar"));
    }

    #[test]
    fn test_palindrome_hello_is_not_palindrome() {
        assert!(!is_palindrome("hello"));
    }

    // โ”€โ”€ fizzbuzz โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    fn test_fizzbuzz_divisible_by_both_returns_fizzbuzz() {
        assert_eq!(fizzbuzz(15), "FizzBuzz");
    }

    #[test]
    fn test_fizzbuzz_divisible_by_3_returns_fizz() {
        assert_eq!(fizzbuzz(9), "Fizz");
    }

    #[test]
    fn test_fizzbuzz_divisible_by_5_returns_buzz() {
        assert_eq!(fizzbuzz(10), "Buzz");
    }

    #[test]
    fn test_fizzbuzz_other_returns_number() {
        assert_eq!(fizzbuzz(7), "7");
    }

    // โ”€โ”€ helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    fn test_assert_approx_eq_passes() {
        assert_approx_eq(0.1 + 0.2, 0.3, 1e-10);
    }

    #[test]
    fn test_assert_sorted_passes() {
        assert_sorted(&[1, 2, 3, 4, 5]);
        assert_sorted(&[1u8]);
        assert_sorted::<i32>(&[]);
    }

    // โ”€โ”€ should_panic example โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    #[should_panic]
    fn test_integer_division_by_zero_panics() {
        let zero = std::hint::black_box(0u32);
        let _ = 5u32 / zero;
    }
}
(* 744: Unit Test Patterns โ€” OCaml with Alcotest-style structure *)

(* The code under test *)
let clamp lo hi x = max lo (min hi x)

let divide_checked a b =
  if b = 0 then None
  else Some (a / b)

let is_palindrome s =
  let n = String.length s in
  let rec check i =
    if i >= n / 2 then true
    else if s.[i] <> s.[n - 1 - i] then false
    else check (i + 1)
  in
  check 0

(* Helper assertion functions *)
let assert_some result =
  match result with
  | Some x -> x
  | None   -> failwith "Expected Some, got None"

let assert_none result =
  match result with
  | None   -> ()
  | Some _ -> failwith "Expected None, got Some"

(* AAA pattern tests *)
let test_clamp_below_lo () =
  (* Arrange *)
  let lo, hi, x = 0, 10, -5 in
  (* Act *)
  let result = clamp lo hi x in
  (* Assert *)
  assert (result = 0)

let test_clamp_within_range () =
  let lo, hi, x = 0, 10, 5 in
  let result = clamp lo hi x in
  assert (result = 5)

let test_clamp_above_hi () =
  let lo, hi, x = 0, 10, 15 in
  let result = clamp lo hi x in
  assert (result = 10)

let test_divide_checked_non_zero () =
  let result = divide_checked 10 3 in
  let v = assert_some result in
  assert (v = 3)

let test_divide_checked_by_zero () =
  assert_none (divide_checked 10 0)

let test_palindrome () =
  assert (is_palindrome "racecar");
  assert (is_palindrome "");
  assert (is_palindrome "a");
  assert (not (is_palindrome "hello"))

let () =
  test_clamp_below_lo ();
  test_clamp_within_range ();
  test_clamp_above_hi ();
  test_divide_checked_non_zero ();
  test_divide_checked_by_zero ();
  test_palindrome ();
  Printf.printf "All OCaml tests passed!\n"