Unit vs Integration Tests
Unit tests exercise one module in isolation inside src/. Integration tests compile as separate crates in tests/ and call only the public API, mirroring how downstream users depend on your crate.
Recipe
// src/lib.rs - unit test
#[cfg(test)]
mod tests {
#[test]
fn internal_helper() { /* uses private fn */ }
}// tests/api_smoke.rs - integration test
use my_crate::Client;
#[test]
fn creates_client() {
let _ = Client::new("http://localhost");
}When to reach for this: Unit tests for logic; integration tests for wiring, HTTP handlers, and DB boundaries.
Working Example
my_service/
├── src/
│ ├── lib.rs
│ ├── domain.rs
│ └── api.rs
└── tests/
├── health.rs
└── orders.rs
// tests/orders.rs
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
use my_service::app;
#[tokio::test]
async fn post_order_returns_201() {
let app = app();
let response = app
.oneshot(Request::builder().method("POST").uri("/orders").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
}What this demonstrates:
- Integration tests are external crates (
use my_service::...) - Only
pubitems are reachable - Async integration tests use
#[tokio::test] - One file per behavior area keeps failures localized
Deep Dive
Comparison
| Aspect | Unit (#[cfg(test)]) | Integration (tests/) |
|---|---|---|
| Location | Inside src/**/*.rs | tests/*.rs |
| Visibility | Private API | Public API only |
| Compile unit | Same crate | Separate crate per file |
| Speed | Faster | Slower (links full lib) |
Multiple Integration Files
Each tests/*.rs is its own crate. Share helpers via tests/common/mod.rs (not auto-discovered as a test) or a tests/common.rs included with mod common;.
Gotchas
- Testing private functions only via unit tests - integration gaps when
pubAPI drifts. Fix: integration tests for every public endpoint or type. - Shared mutable static in parallel tests - flaky failures. Fix:
#[serial_test::serial]or isolate state per test. - Heavy setup duplicated - copy-paste fixtures. Fix: shared
tests/supportmodule (Test Organization). - Integration tests hitting prod DB - dangerous CI flakes. Fix: testcontainers or in-memory fakes.
- One giant integration file - slow incremental compiles. Fix: split by feature area.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Doc tests | Examples in docs must compile | Complex multi-crate wiring |
#[cfg(test)] in tests/common | Shared fixtures | Need to test private helpers |
In-crate mod tests only | Pure algorithm crates | HTTP/CLI boundary testing |
FAQs
How many of each?
Many unit tests for pure logic; fewer integration tests covering critical user paths and error handling.
Can integration tests use dev-dependencies?
Yes. The test crate links against your lib and can use dev-deps declared in the main Cargo.toml.
Why is tests/common.rs not run as a test?
Only tests/*.rs at the top level are test crates. Submodules are support code.
How do I test a binary crate?
Extract logic to a library crate; integration tests import the lib. Binaries stay thin main wrappers.
Workspace integration tests?
Place under the specific package's tests/ or a dedicated integration-tests member crate depending on scope.
Do integration tests run in parallel?
Yes by default. Use --test-threads=1 when tests contend on ports or files.
How do I filter integration only?
cargo test --test orders runs one integration file.
Should handlers be unit tested?
Extract pure logic to functions with unit tests; keep one integration test per route for status codes.
What about #[ignore] integration tests?
Use for docker-required tests; run with cargo test -- --ignored in nightly CI.
How does this relate to nextest?
cargo nextest run respects the same unit/integration split with faster scheduling.
Related
- Testing Basics - assertions and
cargo test - Test Organization - fixtures
- Testing Async Code -
#[tokio::test] - Testing Best Practices - pyramid guidance
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.