---
title: "14.4 Integration tests"
description: "Integration tests live in the tests/ directory, see only a crate's public API, and exercise it the way a real user would: the lib.rs split paying off."
url: "https://learnrust.net/chapter-14/integration-tests/"
last_updated: "2026-06-13"
---

# 14.4 Integration tests

Unit tests (lesson [14.3](https://learnrust.net/chapter-14/organizing-unit-tests/)) check small pieces from the inside, with access to private internals. **Integration tests** do the opposite: they live outside your library, see only its public API, and exercise it exactly the way an external user would. Both kinds matter, and the difference is the whole point. This lesson is also where lesson [13.5](https://learnrust.net/chapter-13/binary-and-library-crates/)'s library/binary split pays the IOU it promised.

## The tests/ directory

Integration tests go in a `tests/` directory at the top of your package, beside `src/` (not inside it):

```
my_lib/
├── Cargo.toml
├── src/
│   └── lib.rs
└── tests/
    └── integration.rs
```

Each file in `tests/` is compiled as its own separate crate that *depends on* your library, just like any external user. So an integration test brings your library into scope with `use` and calls its public functions:

```rust
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn secret_helper() -> i32 {   // private
    42
}
```

```rust
// tests/integration.rs
use my_lib::add;

#[test]
fn adds_through_public_api() {
    assert_eq!(add(2, 3), 5);
}
```

`cargo test` runs these alongside your unit tests, reporting them in their own section:

```
running 1 test
test adds_through_public_api ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```

Notice two things. There's no `#[cfg(test)]` and no `mod tests`: every file in `tests/` is inherently test-only, so the attribute is unnecessary. And the test reaches `add` via `use my_lib::add`, the package name, because the test crate is separate from the library crate.

## They can only see the public API

The defining constraint: an integration test can call `add` (public) but *cannot* call `secret_helper` (private). Try it and the compiler refuses, because from the test crate's vantage point `secret_helper` doesn't exist; it's not part of the library's public surface (lesson [13.3](https://learnrust.net/chapter-13/visibility-pub/)). This is a feature, not a limitation. Integration tests verify the contract you actually promise to users, the public API, free of any knowledge of how it's implemented inside.

> **Key insight**
>
> Unit tests ask "does this internal piece work?"; integration tests ask "does the library work the way a user will actually use it?" Because integration tests see only the public API, they catch a different class of bug: a function that works in isolation but is awkward, mis-wired, or under-exposed when assembled into the real public surface. They also act as a check on your API design: if your own integration test is clumsy to write, your users will find the API clumsy too.

## Why this needs lib.rs

Here's the payoff lesson [13.5](https://learnrust.net/chapter-13/binary-and-library-crates/) promised. An integration test depends on your crate and calls its public functions, which means there *must be* a library crate with a public API to call. A binary crate (just `main.rs`) exposes only an executable; there are no public functions for `tests/` to `use`. So a program whose logic lives entirely in `main.rs` can't be integration-tested at all.

This is precisely why the advice was to put logic in `lib.rs` and keep `main.rs` thin. With the logic in a library crate, your integration tests in `tests/` exercise it fully, and `main.rs` becomes a shell so small it barely needs testing. The two chapters click together: chapter 13 said "structure it this way," and the reason was "so chapter 14 can test it this way." When chapter 20 builds a real command-line tool, this is exactly how it's tested.

## Shared test helpers

Integration tests often need shared setup code (a helper that builds a fixture, say). You can't just put it in a file in `tests/`, because every file there is its own test crate and would try to run the helper as a test. The convention is a subdirectory: `tests/common/mod.rs`. Files in a subdirectory of `tests/` are *not* compiled as separate test crates, so `tests/common/mod.rs` is shared module code that your test files pull in with `mod common;`. You won't need this until your integration tests grow, but it explains the occasional `tests/common/` you'll see in real projects.

## Quiz time

**Question #1**

Where do integration tests live, and how is each file there treated by the compiler?

<details class="solution">
<summary>Show solution</summary>

In a `tests/` directory at the top of the package, beside `src/`. Each file in `tests/` is compiled as its own separate crate that depends on your library, so it sees only the public API and needs no `#[cfg(test)]` (the whole file is test-only by virtue of being in `tests/`).

</details>

**Question #2**

Why can't an integration test call a private function from your library?

<details class="solution">
<summary>Show solution</summary>

Because the integration test is a separate crate that depends on your library and can only see its public API (lesson [13.3](https://learnrust.net/chapter-13/visibility-pub/)). Private items aren't part of that public surface, so from the test crate's perspective they don't exist. This is intentional: integration tests verify the contract you promise to users, not the internals.

</details>

**Question #3**

Why does integration testing require putting logic in `lib.rs` rather than `main.rs`?

<details class="solution">
<summary>Show solution</summary>

Integration tests depend on your crate and call its public functions, so there must be a library crate with a public API. A binary crate (`main.rs` only) exposes just an executable, with no public functions for `tests/` to `use`. Putting the logic in `lib.rs` gives the integration tests something to call; the thin `main.rs` is then almost nothing to test (lesson [13.5](https://learnrust.net/chapter-13/binary-and-library-crates/)).

</details>

You've now got unit tests inside the code and integration tests outside it. There's a third kind you met in chapter 13 without running them: the examples in your doc comments. The next lesson turns those into tests, documentation that can't go stale.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
