๐Ÿฆ€ Functional Rust

747: Test Fixtures: Setup/Teardown, Shared State

Difficulty: 2 Level: Intermediate Use RAII structs for automatic test teardown and `OnceLock` for shared read-only state across tests.

The Problem This Solves

Many tests need setup: create a database, write a config file, seed some state. They also need cleanup: remove those resources afterward, even if the test panics. In languages with `setUp`/`tearDown` lifecycle hooks, teardown is skipped on panic. In Rust, you get something better: RAII. When a fixture struct goes out of scope โ€” including on panic โ€” `Drop` runs. No exceptions. This matters for test isolation: each test gets a fresh fixture, side effects don't leak between tests, and filesystem/network resources are always cleaned up. It also matters for shared resources: some expensive initialization (parsing a large file, building a lookup table) should happen once and be read by many tests. `OnceLock` provides thread-safe lazy initialization with no locks needed after the first access. These patterns compose naturally with Rust's ownership model โ€” the fixture owns its resources, the test owns the fixture, and when the test ends, cleanup is guaranteed by the type system.

The Intuition

A fixture is a struct that owns test resources, initializes them in `new()`, and cleans them up in `Drop::drop()`. Tests create a fixture at the start, use it, and the cleanup is automatic. For shared read-only data, `OnceLock<T>` is a global that initializes exactly once on first access and then hands out `&'static T` references with no runtime cost.

How It Works in Rust

// RAII fixture: teardown guaranteed even on panic
struct DatabaseFixture {
 pub db: Database,
 name: &'static str,
}

impl DatabaseFixture {
 fn new(name: &'static str) -> Self {
     let mut db = Database::new();
     db.insert("user:1", "Alice");
     db.insert("user:2", "Bob");
     println!("[fixture:{}] Set up", name);
     DatabaseFixture { db, name }
 }
}

impl Drop for DatabaseFixture {
 fn drop(&mut self) {
     // Runs even if the test panicked
     println!("[fixture:{}] Torn down", self.name);
 }
}

#[test]
fn test_lookup() {
 let f = DatabaseFixture::new("lookup");
 assert_eq!(f.db.get("user:1"), Some("Alice"));
 // f dropped here โ†’ Drop::drop() called
}

// Shared read-only state: initialized once, read by many tests
static SHARED_DATA: OnceLock<Vec<i32>> = OnceLock::new();

fn shared_data() -> &'static [i32] {
 SHARED_DATA.get_or_init(|| (1..=100).collect())
}

#[test]
fn test_sum() {
 assert_eq!(shared_data().iter().sum::<i32>(), 5050);
}
For shared mutable state across tests, `OnceLock<Mutex<T>>` works โ€” but prefer per-test isolation when possible; test ordering is not guaranteed. The `Mutex` pattern is shown in the example for completeness.

What This Unlocks

Key Differences

ConceptOCamlRust
Setup/teardown`OUnit` `setup`/`teardown` callbacks`Drop` โ€” guaranteed by the type system
Shared stateModule-level `let` (mutable globals are unsafe)`OnceLock<T>` โ€” thread-safe lazy init
Test isolationFunctional purity or manual resetPer-test fixture structs, each owning fresh state
Panic-safe cleanupTry/finally in tests`Drop` runs on panic โ€” no special handling needed
/// 747: Test Fixtures โ€” RAII teardown, shared state, per-test isolation

use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};

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

pub struct Database {
    store: HashMap<String, String>,
}

impl Database {
    pub fn new() -> Self {
        Database { store: HashMap::new() }
    }

    pub fn insert(&mut self, key: &str, value: &str) {
        self.store.insert(key.to_owned(), value.to_owned());
    }

    pub fn get(&self, key: &str) -> Option<&str> {
        self.store.get(key).map(String::as_str)
    }

    pub fn delete(&mut self, key: &str) -> bool {
        self.store.remove(key).is_some()
    }

    pub fn count(&self) -> usize {
        self.store.len()
    }
}

fn main() {
    let mut db = Database::new();
    db.insert("key1", "value1");
    println!("Get key1: {:?}", db.get("key1"));
    println!("Count: {}", db.count());
}

// โ”€โ”€ Test infrastructure โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

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

    // โ”€โ”€ RAII fixture: auto-teardown via Drop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    struct DatabaseFixture {
        pub db: Database,
        name: &'static str,
    }

    impl DatabaseFixture {
        /// Setup: creates a pre-populated database.
        fn new(name: &'static str) -> Self {
            let mut db = Database::new();
            db.insert("user:1", "Alice");
            db.insert("user:2", "Bob");
            db.insert("user:3", "Carol");
            println!("[fixture:{}] Set up ({} entries)", name, db.count());
            DatabaseFixture { db, name }
        }
    }

    impl Drop for DatabaseFixture {
        /// Teardown: runs even if the test panics!
        fn drop(&mut self) {
            println!("[fixture:{}] Torn down", self.name);
        }
    }

    #[test]
    fn test_lookup_existing_user() {
        let f = DatabaseFixture::new("lookup_existing");
        assert_eq!(f.db.get("user:1"), Some("Alice"));
    }

    #[test]
    fn test_lookup_missing_returns_none() {
        let f = DatabaseFixture::new("lookup_missing");
        assert_eq!(f.db.get("user:99"), None);
    }

    #[test]
    fn test_insert_and_retrieve() {
        let mut f = DatabaseFixture::new("insert_retrieve");
        f.db.insert("user:4", "Dave");
        assert_eq!(f.db.get("user:4"), Some("Dave"));
    }

    #[test]
    fn test_delete_reduces_count() {
        let mut f = DatabaseFixture::new("delete");
        let before = f.db.count();
        assert!(f.db.delete("user:1"));
        assert_eq!(f.db.count(), before - 1);
    }

    #[test]
    fn test_delete_nonexistent_returns_false() {
        let mut f = DatabaseFixture::new("delete_nonexistent");
        assert!(!f.db.delete("ghost:999"));
    }

    // โ”€โ”€ Shared read-only fixture via OnceLock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    static SHARED_DATA: OnceLock<Vec<i32>> = OnceLock::new();

    fn shared_data() -> &'static [i32] {
        SHARED_DATA.get_or_init(|| {
            println!("[shared] Initializing shared data (runs once)");
            (1..=100).collect()
        })
    }

    #[test]
    fn test_shared_data_sum() {
        let data = shared_data();
        let sum: i32 = data.iter().sum();
        assert_eq!(sum, 5050);  // sum(1..=100)
    }

    #[test]
    fn test_shared_data_length() {
        assert_eq!(shared_data().len(), 100);
    }

    // โ”€โ”€ Mutex for shared mutable state across tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    // Note: Usually prefer per-test isolation over shared mutable state.

    static COUNTER: OnceLock<Mutex<u32>> = OnceLock::new();

    fn get_counter() -> &'static Mutex<u32> {
        COUNTER.get_or_init(|| Mutex::new(0))
    }

    #[test]
    fn test_counter_increment() {
        let mut guard = get_counter().lock().unwrap();
        let before = *guard;
        *guard += 1;
        assert_eq!(*guard, before + 1);
    }
}
(* 747: Test Fixtures โ€” OCaml *)

(* Setup function: creates a temporary "database" *)
let setup_db () =
  let db = Hashtbl.create 16 in
  Hashtbl.add db "user:1" "Alice";
  Hashtbl.add db "user:2" "Bob";
  db

(* Teardown: in OCaml we just let GC collect it, but we can be explicit *)
let teardown_db db = Hashtbl.reset db

(* Test fixture wrapper โ€” run test then teardown *)
let with_db f =
  let db = setup_db () in
  let result = (try Ok (f db) with e -> Error e) in
  teardown_db db;
  match result with
  | Ok () -> ()
  | Error e -> raise e

(* Tests using the fixture *)
let test_lookup_existing () =
  with_db (fun db ->
    match Hashtbl.find_opt db "user:1" with
    | Some "Alice" -> ()
    | _ -> failwith "expected Alice"
  )

let test_lookup_missing () =
  with_db (fun db ->
    assert (Hashtbl.find_opt db "user:99" = None)
  )

let test_insert_and_retrieve () =
  with_db (fun db ->
    Hashtbl.add db "user:3" "Carol";
    assert (Hashtbl.find_opt db "user:3" = Some "Carol")
  )

let () =
  test_lookup_existing ();
  test_lookup_missing ();
  test_insert_and_retrieve ();
  Printf.printf "Fixture tests passed!\n"