Integration Testing
Integration Test Organization
Because Cargo compiles every top-level .rs file in tests/ into its own separate executable binary, a project with 25 top-level tests/*.rs files must link 25 distinct test binaries during cargo test. Organizing integration tests into a single unified integration test crate (or a small number of domain crates) with submodules dramatically reduces linking time and binary disk usage.
Anti-Pattern (Many Separate Link Steps): Recommended Pattern (Single Unified Binary):
tests/ tests/
├── auth_test.rs (Binary #1) └── integration/
├── billing_test.rs (Binary #2) ├── main.rs (Single Binary!)
├── cache_test.rs (Binary #3) ├── common.rs (Shared helpers)
├── config_test.rs (Binary #4) ├── auth_test.rs (`mod auth_test;`)
└── webhook_test.rs (Binary #5) ├── billing_test.rs(`mod billing_test;`)
└── cache_test.rs (`mod cache_test;`)
(or `tests/integration.rs` + `tests/integration/*.rs`)
How Cargo Treats Top-Level Files vs Subdirectories in tests/
Cargo follows a strict directory rule inside tests/:
- Top-level
.rsfiles (tests/api.rs): Compiled as standalone integration test binaries. - Subdirectories (
tests/common/mod.rsortests/api/auth.rs): Never compiled as standalone binaries on their own; they are only compiled when included viamod ...;from a top-level test file (or whentests/<name>/main.rsis used in Cargo's multi-file test package layout).
| Layout | Number of Linked Test Binaries | dead_code Warnings on Shared Helpers? |
Parallel Execution Across All Integration Tests? |
|---|---|---|---|
Many tests/*.rs files |
$N$ binaries (slow linking) | Yes, if a file doesn't use every helper in common |
Sequential between binaries; parallel only within each binary |
Single tests/integration.rs + tests/integration/*.rs submodules |
1 binary (fast linking) | No (all submodules share the same crate) | Yes (all integration tests run in one shared thread pool) |
Structuring a Single-Binary Integration Test Suite
Create a single entry file tests/integration.rs that declares your domain test submodules located in tests/integration/:
my_crate/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
├── integration.rs <-- The ONLY top-level `.rs` file in `tests/`
└── integration/
├── helpers.rs <-- Shared test fixtures
├── auth.rs <-- `mod auth;`
└── routing.rs <-- `mod routing;`
Inside tests/integration.rs:
// Simulating `tests/integration.rs` with submodules `helpers`, `auth`, and `routing`
mod helpers {
pub fn test_endpoint(path: &str) -> String {
format!("http://127.0.0.1:8080{path}")
}
}
mod auth {
use super::helpers;
#[test]
fn login_endpoint_is_well_formed() {
let url = helpers::test_endpoint("/v1/login");
assert!(url.ends_with("/v1/login"));
}
}
mod routing {
use super::helpers;
#[test]
fn health_endpoint_is_well_formed() {
let url = helpers::test_endpoint("/healthz");
assert!(url.starts_with("http://127.0.0.1:8080"));
}
}
fn main() {
println!("Sample endpoint: {}", helpers::test_endpoint("/status"));
}
Sample endpoint: http://127.0.0.1:8080/status
When you run cargo test --test integration, all tests across auth and routing compile in a single link pass and execute in parallel:
Running tests/integration.rs (target/debug/deps/integration-1a2b3c4d)
running 2 tests
test auth::login_endpoint_is_well_formed ... ok
test routing::health_endpoint_is_well_formed ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s