Running tests in Rust with cargo test is more than just a command; it’s a powerful toolkit for understanding your code’s behavior and pinpointing exactly where things go wrong.
Let’s see it in action. Imagine you have a simple Rust project with a src/lib.rs file:
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two_positive_numbers() {
assert_eq!(add(2, 2), 4);
}
#[test]
fn it_adds_a_positive_and_a_negative_number() {
assert_eq!(add(5, -3), 2);
}
#[test]
fn it_adds_two_negative_numbers() {
assert_eq!(add(-5, -3), -8);
}
}
When you run cargo test in your project’s root directory, you’ll see output like this:
Compiling my_crate v0.1.0 (/path/to/my_crate)
Finished test [unoptimized + debuginfo] target(s) in 0.50s
Running unittests src/lib.rs (target/debug/deps/my_crate-abc123def456.0)
running 3 tests
test tests::it_adds_a_positive_and_a_negative_number ... ok
test tests::it_adds_two_negative_numbers ... ok
test tests::it_adds_two_positive_numbers ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests my_crate
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
This output tells you which tests are running, their status (ok, failed, ignored), and a summary. The #[cfg(test)] attribute is key here; it means the tests module and its contents are only compiled and run when cargo test is invoked.
The core problem cargo test solves is providing a robust and integrated way to verify that your code behaves as expected. It’s not just about finding bugs; it’s about building confidence and enabling refactoring. You can change your code fearlessly, knowing that if a test breaks, you’ve likely introduced a regression.
Internally, cargo test compiles your code with test-specific features enabled. It then executes the compiled test binary. Each function annotated with #[test] becomes an independent test case. The test runner collects the results and reports them. This isolation means one failing test doesn’t stop others from running by default.
You have several levers to control this process:
-
Filtering Tests: If you have many tests, you don’t always want to run them all. You can filter by name. For example, to run only tests whose names contain "adds":
cargo test addsThis will execute
it_adds_two_positive_numbers,it_adds_a_positive_and_a_negative_number, andit_adds_two_negative_numbers. -
Running Specific Test Files/Modules: You can target tests within a specific file or module. To run tests only in
src/main.rs:cargo test --test integration_tests/my_feature.rs(Assuming you have tests in
tests/integration_tests/my_feature.rsfor integration tests. For unit tests insrc/lib.rsorsrc/main.rs, you’d use--libor--binrespectively, though filtering by name is more common for unit tests.) -
Showing Output of Failing Tests: When a test fails, you often need to see the
println!ordbg!output to understand the failure. Use the--nocaptureflag:cargo test -- --nocaptureThe double hyphen (
--) is important here. It separates arguments forcargofrom arguments for the test binary itself.--nocapturetells the test runner not to capture stdout/stderr, so you’ll see any printed output, especially from failing tests. -
Running Only Specific Tests within a File: You can combine name filtering with module paths for more granular control. To run a specific test function
it_adds_two_positive_numberswithin thetestsmodule of your library:cargo test tests::it_adds_two_positive_numbers -
Ignorning Tests: Sometimes you have tests that are slow or broken but you don’t want them to fail your CI. You can mark them with
#[ignore]:#[test] #[ignore] fn it_is_a_slow_test() { // ... }To run ignored tests, use
cargo test -- --ignored. -
Test Organization: Rust distinguishes between unit tests (typically in
src/lib.rsorsrc/main.rswithin#[cfg(test)] mod tests {}) and integration tests (in thetests/directory at the project root).cargo testruns both by default. For integration tests, each file in thetests/directory is compiled and run as a separate crate. You can run specific integration tests by their filename:cargo test tests/api.rs
The most surprising thing about cargo test is how its filtering mechanism works for integration tests. When you pass a path like tests/api.rs, cargo test doesn’t just run that file; it compiles tests/api.rs as its own independent crate, linking against your library crate. This means you can write integration tests that test your library’s public API from an external perspective, just like a user would.
The next concept you’ll likely encounter is how to structure more complex test suites, particularly when dealing with integration tests that require setting up external dependencies or mocking services.