The forty-second loop
You're fixing a bug in your sorting function. You run cargo test. The suite takes forty seconds. The failure is buried in the output of the hundredth test. You fix the code. You run cargo test again. Forty seconds. You repeat this five times. Your coffee goes cold. You're not debugging code anymore; you're debugging your patience.
Rust's test runner is built for speed, but only if you use the filter. Cargo doesn't just execute tests; it builds a binary and passes arguments to it. When you provide a string, you're telling that binary to skip everything that doesn't match. This turns a forty-second suite into a sub-second check. Isolating the test you care about is the first step in a tight feedback loop.
The filter mechanism
Cargo runs tests by compiling your code with a test harness and executing the resulting binary. The harness collects every function marked with #[test] and stores them in a registry. When you pass a string to cargo test, the harness scans that registry. It runs any test whose name contains your string as a substring. The match is case-sensitive. It is not a regular expression. It is a simple substring check.
This design means you can type a fragment of the name and hit enter. You don't need the full function name. You don't need to remember the exact casing of every character. The filter is forgiving enough to be fast but specific enough to narrow the scope.
Minimal example
Start with a library that has a few tests. The goal is to run only the addition test without touching the subtraction logic.
// src/lib.rs
/// Adds two integers.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Subtracts b from a.
pub fn sub(a: i32, b: i32) -> i32 {
a - b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_addition_works() {
assert_eq!(add(2, 2), 4);
}
#[test]
fn test_subtraction_works() {
assert_eq!(sub(5, 3), 2);
}
}
Run the filter from the terminal.
# Run only tests containing "addition"
cargo test test_addition
The output shows one test running. The subtraction test is skipped. The command returns almost instantly. If you run cargo test test_sub, only the subtraction test runs. The filter isolates the work.
Under the hood: the test binary
Understanding the test binary explains why the filter works and unlocks advanced debugging. Cargo compiles your crate into a special executable located in target/debug/deps/. The filename includes a hash, like myapp-abc123def456. That binary contains all your tests and a built-in harness.
You can run that binary directly. The harness accepts the same arguments as cargo test. This is useful when you need to attach a debugger or inspect the binary without Cargo's wrapper.
# Find the test binary
ls target/debug/deps/myapp-*
# Run the binary directly with a filter
./target/debug/deps/myapp-abc123def456 test_addition
Running the binary directly produces the same result as cargo test test_addition. The harness parses the arguments, filters the test list, and executes the matches. This reveals that cargo test is a convenience layer. The real work happens in the compiled test harness.
Convention aside: The community rarely runs the binary directly unless debugging the harness itself or working around a Cargo limitation. Using cargo test keeps the workflow consistent and handles dependencies, environment variables, and target selection automatically. Stick to cargo test for daily work.
Realistic workflow
Real projects have modules, multiple binaries, and doc tests. The filter adapts to this structure. You can filter by module path, target, and test type.
Module paths
When tests live in modules, the full path includes the module hierarchy. You can filter by any part of that path.
// src/math/utils.rs
pub fn complex_calculation(x: f64) -> f64 {
x * 2.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complex_precision() {
let result = complex_calculation(1.5);
assert!((result - 3.0).abs() < f64::EPSILON);
}
}
Filter by the module and test name to disambiguate.
# Run the test in the math::utils module
cargo test math::utils::test_complex
This is essential when you have test_complex in multiple modules. The path filter narrows the scope to the exact location.
Doc tests
Rust runs documentation examples as tests. These live in the doc comments of your items. You can filter doc tests just like unit tests.
/// Adds two numbers.
///
/// # Examples
///
/// ```
/// assert_eq!(my_crate::add(2, 2), 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Run doc tests with the --doc flag. You can combine it with a filter.
# Run doc tests containing "add"
cargo test --doc add
This isolates the documentation examples for the add function. It skips unit tests and other doc tests. This is useful when you're updating docs and want to verify the examples without running the full suite.
Ignored tests
Some tests are slow or require special setup. You can mark them with #[ignore]. These tests are skipped by default. You can run them explicitly.
#[test]
#[ignore]
fn test_slow_integration() {
// Simulates a slow operation
std::thread::sleep(std::time::Duration::from_secs(5));
assert!(true);
}
Run ignored tests with the --ignored flag passed to the harness.
# Run only ignored tests
cargo test -- --ignored
The double dash passes --ignored to the test binary. This flag tells the harness to run tests marked with #[ignore] and skip the rest. You can combine this with a filter.
# Run ignored tests containing "integration"
cargo test --ignored integration
This gives you precise control over slow tests. You can run the fast suite normally and trigger the slow tests only when needed.
Pitfalls and edge cases
The filter is powerful, but a few traps exist. Knowing them prevents wasted time.
The substring trap
The filter matches substrings anywhere in the name. If you name every test test_something, running cargo test test executes the entire suite. The filter matches test in every name. Be specific in your filter string. Use a unique fragment like test_addition instead of test. Name your tests so they filter well.
The double dash separator
Cargo and the test binary have different flags. Cargo handles compilation and target selection. The test binary handles execution and output. You must separate them with --.
# WRONG: Cargo thinks --nocapture is a cargo flag
cargo test test_name --nocapture
# CORRECT: -- passes --nocapture to the test binary
cargo test test_name -- --nocapture
Without the separator, Cargo rejects the command with an error like error: unexpected argument '--nocapture' found. The double dash is the boundary between Cargo and your code. Respect it.
Release mode differences
By default, cargo test builds in debug mode. Optimizations are off. Some bugs only appear in release mode due to inlining, vectorization, or undefined behavior in unsafe code.
# Run tests with release optimizations
cargo test --release test_name
Use --release when you suspect optimization levels affect the result. This compiles the test binary with opt-level = 3. It catches issues that debug mode hides.
Multiple binaries
If your project has multiple binaries, cargo test runs tests for all of them by default. You can target a specific binary.
# Run tests for a specific binary
cargo test --bin my_app test_name
This is useful when you have a library and a CLI tool, and the test lives in the CLI's main.rs or a binary-specific test module.
Thread count
The test harness runs tests in parallel by default. This speeds up the suite but can expose race conditions. If a test fails intermittently, try running it single-threaded.
# Run tests with one thread
cargo test test_name -- --test-threads=1
This forces sequential execution. It helps isolate flaky tests caused by shared state or resource contention.
Decision matrix
Choose the right command for your situation.
Use cargo test substring for rapid iteration on a single feature. Use cargo test module::test_name to disambiguate tests with identical names in different files. Use cargo test --exact test_name when your filter matches multiple tests and you need to run only one. Use cargo test --release when you suspect optimization levels affect the result or need to verify release-mode behavior. Use cargo test name -- --nocapture when you need to see stdout output from a test function. Use cargo test --bin target name when your project has multiple binaries and the test lives in a specific one. Use cargo test --doc filter when you're verifying documentation examples. Use cargo test -- --ignored when you need to run slow or setup-heavy tests. Use cargo test name -- --test-threads=1 when a test fails intermittently and you suspect race conditions.
Name your tests so they filter well. The double dash separates Cargo from your code. Run the binary directly only when you need to debug the harness. Trust the filter to keep your loop tight.