rust-tests-integracion

Integration Tests in Rust with tests and Public API

  • 3 min

A integration test is an external test of the public API of a library.

Unit tests (seen in the previous article) live in the same file as the code. They have a very specific advantage: they can see and test private functions.

Integration Tests are different. They live outside your source code. Rust treats them exactly as if they were external code (another Crate) that is importing your library.

  • They can only call pub functions.
  • They verify that the public API is convenient and works correctly.
  • If you change the internal implementation but not the API, these tests should not break.

The tests/ folder

To create integration tests, we need a specific folder at the same level as src.

my_project/
├── Cargo.toml
├── src/
│   └── lib.rs
└── tests/           <--- HERE
    └── integration.rs
Copied!

Cargo automatically knows that every .rs file inside tests/ is an integration test. No configuration is needed.

Writing the first integration test

Suppose our library (src/lib.rs) has this:

// src/lib.rs
pub fn add_two(a: i32) -> i32 {
    add_internal(a, 2)
}

// Private function (not accessible from integration tests)
fn add_internal(a: i32, b: i32) -> i32 {
    a + b
}
Copied!

Now we create the file tests/my_test.rs:

// tests/my_test.rs

// 1. We must import our library as if it were external "use crate_name"
use my_project;

#[test]
fn public_api_test() {
    // We can only access what is 'pub'
    assert_eq!(my_project::add_two(2), 4);
}
Copied!

When you run cargo test, you will see a new section:

Running tests/my_test.rs (target/debug/deps/my_test-...)
test public_api_test ... ok
Copied!

Key differences

  1. We don’t need #[cfg(test)]: Files in tests/ are only compiled when we run tests, so the module annotation is not necessary.
  2. Each file is a Crate: Rust compiles each file inside tests/ as if it were an independent binary. This is important for understanding how to share code.

Sharing setup code

Imagine you have 5 test files and all of them need a setup function (for example, create_test_user()).

If you create a file tests/common.rs, Rust will think it is another test and will try to run it, which we don’t want.

The correct way to do this is by using Rust’s module structure: Create a subfolder.

File structure:

tests/
├── login_test.rs
└── common/
    └── mod.rs       <--- Helper code
Copied!

By placing it in common/mod.rs, Rust does not consider it an independent test, but a module that other tests can import.

In tests/common/mod.rs:

pub fn setup() {
    // Code to prepare database, etc.
    println!("Setting up test environment...");
}
Copied!

In tests/login_test.rs:

use my_project;
mod common; // Import the helper module

#[test]
fn login_works() {
    common::setup(); // Call the setup
    assert!(true);
}
Copied!

Testing binaries (main.rs)

Here is a small limitation of Rust: Integration tests are designed to test Libraries (lib.rs). If your project is just a binary (src/main.rs), you cannot use my_project from the tests/ folder, because binaries do not expose functions.

A common structure: If the program grows, we can move the logic to lib.rs and leave main.rs as a thin layer that calls the library.

// src/main.rs
use my_project::start;

fn main() {
    // The main is so simple it doesn't need testing
    start();
}
Copied!

This way you can test all the logic of start and its components via integration tests in the library.