🎓 A1Learner
🔍

Rust Integration Tests

An integration test is a test that stands outside the crate and uses only its public parts. Unit tests live inside the crate and may call private helpers.

The folder shape

Integration tests live in a top-level tests/ folder. It sits beside src/, not inside it:

src/lib.rs            the library itself
tests/api.rs          one integration test file
tests/common/mod.rs   helpers shared by test files

A first integration test

The file imports the library by its crate name. Every call goes through the public API:

use shop::Basket;

#[test]
fn empty_basket_totals_zero() {
    let basket = Basket::new();
    assert_eq!(basket.total(), 0);
}

Private items stay private

The helper half has no pub marker. Unit tests inside the crate may call it. This file may not:

use shop::half;

Share setup in a common folder

Many test files need the same starting state. Build it once in tests/common/mod.rs:

use shop::Basket;

pub fn basket_with_books() -> Basket {
    let mut basket = Basket::new();
    basket.add(1200);
    basket.add(800);
    basket
}

Borrow the shared setup

Each test file pulls the folder in with mod common;. The tests stay short:

mod common;

#[test]
fn two_books_total() {
    let basket = common::basket_with_books();
    assert_eq!(basket.count(), 2);
    assert_eq!(basket.total(), 2000);
}

Crates that only tests may use

itoa turns numbers into text. The library never needs it. It is declared as a dev-dependency, so tests may use it:

let mut buffer = itoa::Buffer::new();
assert_eq!(buffer.format(basket.total()), "2000");

Example

struct Basket {
    items: Vec<i32>,
}

impl Basket {
    fn new() -> Basket {
        Basket { items: Vec::new() }
    }

    fn add(&mut self, price: i32) {
        self.items.push(price);
    }

    fn total(&self) -> i32 {
        self.items.iter().sum()
    }

    fn count(&self) -> usize {
        self.items.len()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_basket_totals_zero() {
        assert_eq!(Basket::new().total(), 0);
    }

    #[test]
    fn two_items_count_two() {
        let mut basket = Basket::new();
        basket.add(1200);
        basket.add(800);
        assert_eq!(basket.count(), 2);
    }
}

fn main() {
    let mut basket = Basket::new();
    basket.add(1200);
    basket.add(800);
    println!("count = {}", basket.count());
    println!("total = {}", basket.total());
    println!("tests/api.rs checks this same basket from outside");
}

Key points

  • Integration test: a test in the tests/ folder.
  • It uses the crate from the outside, like real user code.
  • Use one file per area you test.
  • Cargo builds each file in tests/ as its own small crate.
  • Use cargo test --test api to run one integration file on its own.
  • Replace api with the file name, without the .rs.
  • Helper crates for tests belong under [dev-dependencies] in Cargo.toml.
  • The shipped library never includes them.
  • Move the real work into lib.rs.
  • Keep main.rs thin.

In this example, a Basket holds prices in cents. add puts one price in. total adds the prices up. count counts them. The unit tests sit inside the file, in the usual cfg(test) module. main builds the same basket the snippets used. It prints count = 2 and total = 2000. The last line points at the other half of the story. A file in tests/ can build this basket too. It just cannot see anything that lacks pub.

Note

A crate with only main.rs cannot be integration tested.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why does an integration test fail to compile when it calls a private function?