Testing

Rust's testing tools are built into Cargo. Unit tests live next to the code. Integration tests live in a sibling directory. Everything runs with cargo test. There is no test framework to pick.

Unit tests: in the same file

fn add(a: i32, b: i32) -> i32 {
    a + b
}
 
#[cfg(test)]
mod tests {
    use super::*;
 
    #[test]
    fn adds_positive_numbers() {
        assert_eq!(add(2, 3), 5);
    }
 
    #[test]
    fn adds_negatives() {
        assert_eq!(add(-2, -3), -5);
    }
}

Three things to notice:

  • #[cfg(test)] excludes the test module from non-test builds.
  • use super::*; imports the parent module so tests can call private functions.
  • #[test] marks a function as a test. It must take no arguments and return () (or Result<(), E>).

This is the canonical shape. Sail uses it consistently across the workspace.

Integration tests: in tests/

my-crate/
├── src/
│   └── lib.rs
└── tests/
    └── catalog.rs       # Each file in tests/ is its own crate.
tests/catalog.rs
use my_crate::CatalogManager;
 
#[test]
fn manager_creates_default_catalog() {
    let m = CatalogManager::default();
    assert_eq!(m.catalogs().count(), 1);
}

Integration tests can only call your crate's public API. They prove the public surface works without being able to reach private internals.

Use integration tests for end-to-end behavior and public-API contracts. Use unit tests for internal correctness.

Async tests

#[tokio::test]
async fn handles_concurrent_requests() {
    let server = start_test_server().await;
    let result = server.request("hello").await.unwrap();
    assert_eq!(result, "world");
}

#[tokio::test] builds a runtime and runs the test on it. Substitute your async runtime's macro if you use a different one.

Assertions

MacroUse for
assert!(cond)Boolean condition.
assert_eq!(a, b)Equality (prints both sides on failure).
assert_ne!(a, b)Inequality.
assert!(matches!(value, Variant(_)))Pattern match on an enum without binding.

Always prefer assert_eq! over assert!(a == b). The diagnostic on failure is much better.

Error-path tests

The most-neglected category. Every Err variant should have at least one test that produces it:

#[test]
fn rejects_missing_port() {
    let err = parse_config("{}").unwrap_err();
    assert!(matches!(err, ConfigError::MissingField { .. }));
}
 
#[test]
fn rejects_invalid_json() {
    let err = parse_config("{not json").unwrap_err();
    assert!(matches!(err, ConfigError::Json(_)));
}

This is one of the 12 failure modes — agents test the happy path and skip the error paths.

Test helpers

Use #[cfg(test)] modules to expose helpers only during testing:

#[cfg(test)]
pub(crate) mod test_helpers {
    pub fn dummy_user() -> super::User {
        super::User { id: 1, name: "test".into() }
    }
}

Now test_helpers::dummy_user() is available in tests but not in production builds.

Property tests

For invariant-rich code, use proptest:

use proptest::prelude::*;
 
proptest! {
    #[test]
    fn round_trips(n in 0u64..) {
        let bytes = n.to_le_bytes();
        let back = u64::from_le_bytes(bytes);
        prop_assert_eq!(n, back);
    }
}

proptest generates many random inputs and shrinks failing ones to the minimal case. For codecs, parsers, math, this is a huge force multiplier.

Snapshot tests

For output that is verbose but stable, use insta:

use insta::assert_snapshot;
 
#[test]
fn formats_query_plan() {
    let plan = build_plan();
    assert_snapshot!(plan.to_string());
}

cargo insta review opens a TUI to accept or reject snapshot changes. Useful for query plan rendering, code generation, error messages, large structures.

Running tests

cargo test                          # Run all
cargo test catalog                  # Run tests whose name matches "catalog"
cargo test -- --nocapture           # Don't capture stdout
cargo test -- --test-threads=1      # Run serially (for tests with shared state)
cargo nextest run                   # Parallel test runner with better output (recommended)

For production projects, install cargo-nextest. It is faster, runs tests in separate processes (so a panic doesn't kill the runner), and gives much better failure output.

Doc tests

Code inside doc comments runs as tests:

/// Add two numbers.
///
/// ```
/// assert_eq!(my_crate::add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

cargo test --doc runs all doc tests. Worth using for crate examples; the test ensures the example actually compiles.

The gates

Reiterating the Cargo gates here for context:

cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --workspace

If you remember nothing else: this is the floor for any agent-written PR.