Integration Testing in Rust

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

FeatureUnit TestIntegration Test
LocationInside the moduleIn tests/ directory
ScopePrivate + public itemsOnly public API
CompilationSame crateSeparate crate
PurposeTest individual unitsTest 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.