Skip to Content
Course content

210: Integration Tests Directory Structure

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a teammate who was building a complex client for a cloud storage API. He had spent the afternoon meticulously organizing his integration tests. He'd created a tests/ directory and, wanting to be clean, he created a file called tests/setup.rs to handle the API authentication and bucket initialization logic. He then tried to call those setup functions from his actual test files. The problem? He was staring at a wall of compiler errors and a very confusing cargo test output that insisted setup.rs was its own test suite, but it contained no tests to run.

This is a classic Rust "gotcha." Because of how Cargo handles the tests/ directory, it doesn't work like a standard module tree. If you aren't careful, you'll end up with a project that feels like it's fighting you every time you try to share a single helper function.

The Boundary of the Public API

In Rust, unit tests live inside your src/ files, often in a nested mod tests. Those tests have "insider access" to your private functions. Integration tests, however, live in the top-level tests/ directory. When you put a file here, Cargo compiles it as a separate crate.

This is intentional. By forcing the integration tests into their own crates, Rust ensures you are testing your library exactly how a user would: by calling your public API. If you find yourself wishing you could test a private function in an integration test, you're actually discovering that the function probably belongs in a separate internal module that should be public to your own crate, but not to the world.

For example, if you have a library called cloud_vault, your integration test file tests/upload_tests.rs will start with use cloud_vault::VaultClient;. You cannot use use crate::... because, as far as the compiler is concerned, the test file is a completely different program than the library it's testing.

Avoiding the Compilation Trap with Shared Modules

Now, back to my teammate's struggle. If every .rs file directly under tests/ is treated as a separate crate, then tests/setup.rs becomes its own test executable. Cargo sees it, tries to run it, finds no #[test] functions, and warns you—all while your other tests fail to import it because you can't easily import from one test crate into another.

The fix is simple but specific: move shared logic into a sub-directory. Cargo only treats files in the root of tests/ as separate crates. Anything in a sub-folder is ignored by the test runner and can be treated as a normal module.

The standard pattern looks like this:

project_root/
├── src/
│   └── lib.rs
└── tests/
    ├── common/
    │   └── mod.rs  <-- Shared helper functions go here
    ├── upload_tests.rs
    └── download_tests.rs

In tests/common/mod.rs, you define your setup logic. Then, in tests/upload_tests.rs, you can simply add mod common;. Now, common is a module within the upload_tests crate, not a standalone crate itself. I've found that this is the only way to keep a large integration suite from becoming a repetitive mess of boilerplate code.




📋 Practical Task

Structuring a Mock API Client Integration Suite

You are building a library that interacts with a weather API. You need to set up an integration test suite that shares a common "Mock Server" setup to avoid duplicating the server start/stop logic across multiple test files.

Requirements:

  • Create a tests/ directory in your project root.
  • Create a sub-directory tests/common/ with a mod.rs file. Inside mod.rs, define a public function setup_mock_server() that returns a String (representing the server URL).
  • Create a test file tests/weather_tests.rs.
  • In weather_tests.rs, declare the common module and write a test function test_get_temperature() that calls common::setup_mock_server() and asserts that the returned URL is not empty.

Goal: Run cargo test and verify that only weather_tests is executed as a test crate, and that it successfully utilizes the logic inside the common module without Cargo attempting to run common as a standalone test.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.