Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing with Stillwater

Test domain decisions as ordinary functions, then test the shell’s observable ordering, error propagation, and persisted state using explicit service implementations.

The snippets below execute their assertions as Cargo doctests. In application test files, place equivalent bodies in ordinary Rust or Tokio tests. Do not hide doctest examples inside test-only modules: a passing doctest can otherwise contain unchecked code.

Pure decisions and accumulated errors

use stillwater::{Validation, assert_success, assert_failure, assert_validation_errors};

fn adult(age: u8) -> Validation<u8, Vec<&'static str>> {
    if age >= 18 { Validation::success(age) } else { Validation::failure(vec!["underage"]) }
}

assert_success!(adult(18));
assert_failure!(adult(17));
assert_validation_errors!(adult(17), vec!["underage"]);

let checks = Validation::<u8, Vec<&str>>::all((adult(16), adult(17)));
assert_eq!(checks.into_result(), Err(vec!["underage", "underage"]));

Prefer asserting exact outputs and errors over checking only success/failure. Include boundary values and verify that independent failures accumulate.

Explicit test environments

Small environments often need no builder. MockEnv is available when tuple composition helps construct a fixture; each with adds one nested pair.

use stillwater::testing::MockEnv;

let (((), minimum_age), users) = MockEnv::new()
    .with(|| 18_u8)
    .with(|| Vec::<String>::new())
    .build();

assert_eq!(minimum_age, 18);
assert!(users.is_empty());

Shell ordering and failure boundaries

use std::sync::{Arc, Mutex};
use stillwater::prelude::*;

#[derive(Clone, Default)]
struct Env { events: Arc<Mutex<Vec<&'static str>>> }

tokio_test::block_on(async {
    let env = Env::default();
    let save = from_fn(|env: &Env| {
        env.events.lock().unwrap().push("save");
        Err::<(), _>("database unavailable")
    });
    let workflow = save.and_then(|()| from_fn(|env: &Env| {
        env.events.lock().unwrap().push("email");
        Ok(())
    }));
    assert_eq!(workflow.run(&env).await, Err("database unavailable"));
    assert_eq!(*env.events.lock().unwrap(), vec!["save"]);
});

The registration example also tests stale availability facts at commit time, rejected inputs, hashing failure, save failure, and email failure after persistence. Check persisted state as well as return values: an error does not imply that earlier operations were rolled back.

Property tests

Add proptest as a development dependency. Stillwater’s optional proptest feature also supplies Arbitrary implementations for supported library types. A runner can execute a property directly, which works in a doctest as well as in a normal test.

use proptest::{prelude::*, test_runner::TestRunner};
use stillwater::Validation;

TestRunner::default().run(&any::<i32>(), |value| {
    let result = Validation::<_, Vec<String>>::success(value).map(|x| x);
    prop_assert_eq!(result, Validation::Success(value));
    Ok(())
}).unwrap();

Choose domain properties too: for example, rejected registration decisions never emit plans, and independent validation errors retain deterministic order. Bound arithmetic inputs or use checked operations so overflow does not accidentally become the property.

Run the relevant targets

cargo test
cargo test --all-features
cargo test --doc --all-features
cargo test --example user_registration --all-features

Default Cargo/nextest discovery does not run the tests embedded in this example. CI selects it explicitly. The documentation contract also rejects test-only attributes inside executable snippets and checks that every book chapter is in the Cargo manifest.