Unit Testing

Running Tests with Cargo

The cargo test command compiles a package's targets in test mode and runs the resulting test binaries sequentially. Understanding the execution order, target selection flags, and separator syntax (--) allows you to run fast, targeted test loops during development.

`cargo test` Default Execution Pipeline:
  1. Unit tests in library target        (`src/lib.rs`)
  2. Unit tests in binary target(s)      (`src/main.rs`, `src/bin/*.rs`)
  3. Integration tests                   (`tests/*.rs`, one binary per file)
  4. Documentation tests                 (`rustdoc --test` on `src/lib.rs`)

If any step in this pipeline fails, cargo test stops immediately unless you pass --no-fail-fast.

Cargo Flags vs Test Binary Flags (-- Separator)

cargo test accepts two distinct sets of flags separated by --:

cargo test [CARGO_OPTIONS] [TESTNAME_FILTER] -- [TEST_BINARY_OPTIONS]
Flag Placement Purpose
--lib Before -- Runs only the library's unit tests (src/lib.rs), skipping binaries, integration tests, and doc-tests.
--bins / --bin <name> Before -- Runs only unit tests inside binary targets (src/main.rs or src/bin/<name>.rs).
--test <name> Before -- Runs only the specified integration test file tests/<name>.rs.
--doc Before -- Runs only documentation tests.
--no-run Before -- Compiles all test binaries without executing them (useful in CI to separate build time from test execution time).
--no-fail-fast Before -- Continues running remaining test targets even if an earlier target fails.
--release Before -- Compiles and runs tests with [profile.release] optimizations enabled.
--test-threads=1 After -- Instructs the test harness binary to run tests sequentially on a single thread instead of in parallel.
--show-output / --nocapture After -- Instructs the test harness binary to display standard output for passing tests.

Controlling Test Parallelism (--test-threads)

By default, the Rust test harness runs all tests in a binary concurrently across a thread pool sized to the host's logical CPU count. Because tests run in parallel within the same OS process:

  • Tests must not contend over the same fixed file path (e.g., /tmp/test.db), fixed TCP port (127.0.0.1:8080), or global process state (env::set_current_dir).
  • Use unique temporary paths per test and ephemeral ports (127.0.0.1:0), or force single-threaded execution when testing unavoidable global state:
cargo test -- --test-threads=1

You can also set the RUST_TEST_THREADS=1 environment variable to control thread parallelism without passing -- --test-threads=1 on the command line.

Verifying Behavior in Both Debug and Release Modes

Because integer overflow checks and debug_assert! macros are enabled in the default test profile (which inherits from dev) and disabled in release (cargo test --release), running both cargo test and cargo test --release in continuous integration catches both debug-assertion failures and optimization-dependent edge cases:

pub fn wrapping_increment(x: u8) -> u8 {
    x.wrapping_add(1)
}

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

    #[test]
    fn wraps_around_u8_max_in_both_profiles() {
        assert_eq!(wrapping_increment(u8::MAX), 0);
    }
}

fn main() {
    println!("255 + 1 (wrapping) = {}", wrapping_increment(u8::MAX));
}
255 + 1 (wrapping) = 0