Integration testing in Rust involves placing test files in a dedicated tests/ directory outside your main code. These tests interact with your library or binary the way an external user would, and they are compiled as separate crates, giving you full isolation for end-to-end behaviour validation.
After getting comfortable with unit testing, I needed a way to test my code from the outside as if I were using it as a library or CLI tool. That is when I discovered Rust’s integration testing system. It requires almost no configuration and works out of the box with cargo test. Writing integration tests helped me catch real-world bugs I missed in unit tests.
This article shows how to set up, structure, and run integration tests in Rust, and how they differ from unit tests.
What Are Integration Tests?
Integration tests:
- Live in the tests/ folder at the project root
- Are compiled as separate crates
- Only have access to your public functions
- Simulate how a real user would use your code
This makes them ideal for testing the overall behavior and public API.
Creating an Integration Test File
Create a new file in the tests/ folder:
project/
├── src/
│ └── lib.rs
└── tests/
└── my_test.rs
In tests/my_test.rs, write:
use my_crate_name::some_function;
#[test]
fn test_behavior() {
let result = some_function(2);
assert_eq!(result, 4);
}
Replace my_crate_name with the actual name in your Cargo.toml.
To run:
cargo test
Rust will automatically discover and run integration tests alongside your unit tests.
Accessing Public Code
Integration tests can only access public items.
If your function is private (fn helper()), it will not be visible. You must mark it as pub in lib.rs or mod.rs:
pub fn helper() {
// exposed to integration tests
}
This enforces real user access, your integration tests act like an external crate.
Organizing Multiple Test Files
Each file in the tests/ directory becomes a separate crate.
For example:
tests/
├── api.rs
├── math.rs
├── strings.rs
Each file is compiled and run independently, allowing good separation of concerns.
To run only one test file:
cargo test --test math
Shared Setup with a Common Module
If you need shared setup or utilities, create a mod.rs or common.rs file:
// tests/common.rs
pub fn setup() {
// setup code like reading config or creating test data
}
Then include it in your test files:
mod common;
#[test]
fn uses_shared_setup() {
common::setup();
// run test
}
This avoids duplication across files.
Example Project
Let us say your lib.rs contains:
pub fn greet(name: &str) -> String {
format!("Hello, {}", name)
}
Your tests/greeting.rs file can contain:
use my_crate_name::greet;
#[test]
fn test_greeting() {
let msg = greet("Rust");
assert_eq!(msg, "Hello, Rust");
}
This simulates how someone importing your crate would test the output.
Integration Test vs Unit Test
Feature | Unit Test | Integration Test |
---|---|---|
Location | Inside the module | In tests/ directory |
Scope | Private + public items | Only public API |
Compilation | Same crate | Separate crate |
Purpose | Test individual units | Test overall behavior |
Use both to cover internal logic and full usage scenarios.
Integration Testing a Binary Crate
If you have a CLI tool in src/main.rs, move logic into src/lib.rs so tests can import it.
Then test logic in lib.rs from the tests/ folder.
This keeps main.rs small and your logic testable.
Summary
Integration testing in Rust is easy and built-in. By placing test files in the tests/ directory, you test your public API from an external perspective. Each file is a separate crate, giving you clean isolation. Combined with unit tests, integration tests provide complete coverage and help ensure your code behaves as expected in real scenarios.
Rust Intermediate Concepts
- Generic Types in Functions and Structs
- Implementing Traits for Custom Behavior in Rust
- Trait Bounds and Constraints in Rust
- Lifetimes in Rust
- Using Closures (Anonymous Functions) in Rust
- The Iterator Trait and .next() in Rust
- Higher Order Iterator Methods: map, filter, and fold in Rust
- Using impl Trait for Simplicity in Rust
- Advanced Collections: HashSet and BTreeMap in Rust
- Custom Error Types and the Error Trait in Rust
- Option and Result Combinators in Rust
- Writing Unit Tests in Rust
- Integration Testing in Rust
- Logging in Rust with the log Crate
- Cargo Tips and Tricks for Rust Projects
- CLI Argument Parsing with Clap in Rust
- File I/O and File System Operations in Rust
- Running External Commands in Rust
- Make HTTP Requests in Rust with Reqwest
- JSON Serialization and Deserialization in Rust with Serde
- Building a Weather CLI in Rust
- Date and Time Handling in Rust with Chrono
- Using Regular Expressions in Rust with the regex Crate
- Memory Management in Rust
- Understanding Borrow Checker Errors in Rust
- Interacting with Databases in Rust
- Building a Todo List CLI in Rust with SQLite
- Concurrency in Rust