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.
Search across all documentation pages
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.
// 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.
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:
use my_service::...)pub items are reachable#[tokio::test]| 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) |
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;.
pub API drifts. Fix: integration tests for every public endpoint or type.#[serial_test::serial] or isolate state per test.tests/support module (Test Organization).| 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 |
Many unit tests for pure logic; fewer integration tests covering critical user paths and error handling.
Yes. The test crate links against your lib and can use dev-deps declared in the main Cargo.toml.
Only tests/*.rs at the top level are test crates. Submodules are support code.
Extract logic to a library crate; integration tests import the lib. Binaries stay thin main wrappers.
Place under the specific package's tests/ or a dedicated integration-tests member crate depending on scope.
Yes by default. Use --test-threads=1 when tests contend on ports or files.
cargo test --test orders runs one integration file.
Extract pure logic to functions with unit tests; keep one integration test per route for status codes.
Use for docker-required tests; run with cargo test -- --ignored in nightly CI.
cargo nextest run respects the same unit/integration split with faster scheduling.
cargo test#[tokio::test]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+.
Reviewed by Chris St. John·Last updated Jul 16, 2026