Test organization is crucial for maintaining a robust, scalable, and understandable test suite, especially as a project grows. In Rust, test organization is highly structured and leverages the module system and `cargo test` command effectively. Proper organization ensures that tests are easy to write, run, debug, and maintain.
Here are the primary ways Rust organizes tests:
1. Unit Tests:
* Location: Unit tests are typically placed in the same file as the code they are testing. This is usually within a dedicated `mod tests { ... }` block at the bottom of the file.
* Visibility: They often test private functions and internal implementation details, which is why co-locating them with the code is beneficial.
* Conditional Compilation: The `mod tests { ... }` block is almost always annotated with `#[cfg(test)]`. This attribute tells Rust to compile and run the code inside this module *only* when `cargo test` is executed, not during a regular `cargo build`. This prevents test code from being included in the final compiled binary, saving space and compilation time.
* Syntax: Each test function within the `tests` module is marked with the `#[test]` attribute.
2. Integration Tests:
* Location: Integration tests are placed in a separate directory named `tests` at the root of your project, alongside `src` and `Cargo.toml`.
* Purpose: These tests verify the public API of your library crate and how different parts of your code interact with each other. They treat your library as an external dependency, just like an end-user would.
* Structure: Each file in the `tests/` directory is compiled as its own separate crate. This means you need to bring your library into scope with `use <your_crate_name>::*` (or specific modules).
* Helper Modules: For common setup, utilities, or shared data across multiple integration test files, you can create a `common` module (e.g., `tests/common/mod.rs`). Then, in your integration test files, you can import it with `mod common;`. Rust will not compile `common/mod.rs` as a separate test crate but will treat it as a regular module.
3. Documentation Tests (Doc Tests):
* Location: These tests are embedded directly within your public function or struct documentation comments (the `///` style).
* Purpose: They serve two roles: providing executable examples of how to use your code and ensuring those examples remain correct and up-to-date with your API changes.
* Execution: `cargo test` automatically finds and runs these code examples.
Key Principles for Test Organization:
* Clarity: Tests should be easy to read and understand.
* Isolation: Tests should ideally be independent of each other.
* Speed: Faster tests encourage more frequent running. Unit tests are generally faster than integration tests.
* Completeness: Aim for good test coverage, balancing unit and integration tests.
By following these organizational principles, Rust developers can build robust and maintainable applications with confidence in their correctness.
Example Code
// Project structure:
// my_project/
// ├── Cargo.toml
// ├── src/
// │ └── lib.rs
// └── tests/
// ├── common/
// │ └── mod.rs
// └── integration_test.rs
// --- src/lib.rs ---
/// A simple struct representing a Point in 2D space.
pub struct Point {
pub x: i32,
pub y: i32,
}
impl Point {
/// Creates a new Point.
///
/// # Examples
///
/// ```
/// let p = my_crate::Point::new(10, 20);
/// assert_eq!(p.x, 10);
/// assert_eq!(p.y, 20);
/// ```
pub fn new(x: i32, y: i32) -> Point {
Point { x, y }
}
/// Calculates the Manhattan distance from the origin (0,0).
///
/// # Examples
///
/// ```
/// let p = my_crate::Point::new(3, 4);
/// assert_eq!(p.manhattan_distance(), 7);
/// ```
pub fn manhattan_distance(&self) -> i32 {
self.x.abs() + self.y.abs()
}
// A private helper function (only accessible by unit tests in this file)
fn _private_helper(&self) -> i32 {
self.x * 2 + self.y * 2
}
}
/// A function to add two numbers.
///
/// # Examples
///
/// ```
/// assert_eq!(my_crate::add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// --- Unit Tests (in the same file, hidden from regular builds) ---
#[cfg(test)]
mod tests {
// Import everything from the parent module (lib.rs)
// so we can test its items, including private ones.
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
assert_eq!(add(-1, 1), 0);
assert_eq!(add(0, 0), 0);
}
#[test]
fn test_point_new() {
let p = Point::new(5, 10);
assert_eq!(p.x, 5);
assert_eq!(p.y, 10);
}
#[test]
fn test_point_manhattan_distance() {
let p1 = Point::new(3, 4);
assert_eq!(p1.manhattan_distance(), 7); // |3| + |4| = 7
let p2 = Point::new(-1, 2);
assert_eq!(p2.manhattan_distance(), 3); // |-1| + |2| = 3
let p3 = Point::new(0, 0);
assert_eq!(p3.manhattan_distance(), 0);
}
#[test]
fn test_private_helper_function() {
let p = Point::new(1, 2);
assert_eq!(p._private_helper(), 6); // 1*2 + 2*2 = 6
}
#[test]
#[should_panic(expected = "assertion failed")] // Example of panic test
fn test_panic_example() {
assert!(false); // This will panic
}
#[test]
#[ignore = "This test is ignored by default, run with `cargo test -- --ignored`"]
fn expensive_test() {
// Imagine some long-running operation here
assert!(true);
}
}
// --- tests/common/mod.rs (for shared setup among integration tests) ---
pub fn setup_test_environment() {
// Perform any common setup here, like initializing a database,
// setting up mock data, logging configuration, etc.
println!("--- Setting up test environment (common) ---");
}
pub fn teardown_test_environment() {
// Perform any common teardown here
println!("--- Tearing down test environment (common) ---");
}
// A helper struct or function
pub struct TestConfig {
pub api_key: String,
pub base_url: String,
}
impl TestConfig {
pub fn load() -> Self {
println!("Loading test config...");
TestConfig {
api_key: "TEST_API_KEY".to_string(),
base_url: "http://localhost:8080".to_string(),
}
}
}
// --- tests/integration_test.rs (an example integration test file) ---
// Import the common module for setup/teardown
mod common;
// Bring the library's public items into scope
use my_crate::{add, Point};
#[test]
fn test_add_integration() {
// Use common setup
common::setup_test_environment();
let result = add(10, 20);
assert_eq!(result, 30);
// Use common teardown (or defer it)
common::teardown_test_environment();
}
#[test]
fn test_point_creation_and_distance_integration() {
common::setup_test_environment();
let config = common::TestConfig::load();
println!("API Key for test: {}", config.api_key); // Example using common config
let p = Point::new(5, 5);
assert_eq!(p.x, 5);
assert_eq!(p.y, 5);
assert_eq!(p.manhattan_distance(), 10);
common::teardown_test_environment();
}
// How to run tests:
// To run all tests (unit, integration, and doc tests): `cargo test`
// To run only ignored tests: `cargo test -- --ignored`
// To run specific unit tests (e.g., in src/lib.rs): `cargo test test_add`
// To run specific integration tests (e.g., in tests/integration_test.rs):
// `cargo test --test integration_test test_add_integration`
// To run only unit tests: `cargo test --lib`
// To run only integration tests: `cargo test --tests`








Test Organization in Rust