24.3 Test Organization
Rust’s testing framework encourages separating tests based on their scope: unit tests and integration tests.
24.3.1 Unit Tests
Unit tests verify small, isolated components, typically individual functions or methods, including private ones. They are conventionally placed within the same source file as the code under test, inside a dedicated submodule named tests
and annotated with #[cfg(test)]
.
// In src/lib.rs or src/my_module.rs
pub fn process_data(data: &[u8]) -> Result<String, &'static str> {
if data.is_empty() {
return Err("Input data cannot be empty");
}
internal_helper(data)
}
// Private helper function
fn internal_helper(data: &[u8]) -> Result<String, &'static str> {
// ... complex logic ...
Ok(format!("Processed {} bytes", data.len()))
}
// Unit tests are placed in a conditionally compiled submodule
#[cfg(test)] // Ensures this module is only compiled during `cargo test`
mod tests {
use super::*; // Import items from parent module (process_data, internal_helper)
#[test]
fn test_process_data_success() {
let result = process_data(&[1, 2, 3]).unwrap();
assert_eq!(result, "Processed 3 bytes");
}
#[test]
fn test_process_data_empty() {
let result = process_data(&[]);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Input data cannot be empty");
}
#[test]
fn test_internal_logic() {
// Directly test the private helper function
let result = internal_helper(&[10]).unwrap();
assert!(result.contains("1 bytes")); // Example check
}
}
#[cfg(test)]
: This attribute ensures that thetests
module and its contents are only included when compiling for tests (cargo test
). This avoids including test code in release builds.use super::*;
: This imports all items (functions, types, etc.) from the parent module (super
), making them available within thetests
module.- Testing Private Items: Unit tests can directly access and test private functions and types within the same module (like
internal_helper
). This is useful for verifying internal implementation details or invariants that are not exposed publicly.
Cargo’s cargo new my_lib --lib
command automatically generates a src/lib.rs
file with this standard test module structure.
24.3.2 Integration Tests
Integration tests verify the public API of your library crate from an external perspective, mimicking how other crates would use it. They reside in a dedicated tests
directory at the root of your project, alongside the src
directory.
my_crate/
├── Cargo.toml
├── src/
│ └── lib.rs // Contains process_data, internal_helper (private)
└── tests/ // Integration tests directory
├── common/ // Optional subdirectory for shared test utilities
│ └── mod.rs // Module file for common utilities
└── api_usage_tests.rs // An integration test file
Each .rs
file within the tests
directory (e.g., api_usage_tests.rs
) is compiled by Cargo as a separate crate. This means each such test file links against your library crate (my_crate
in this example) as if it were an external dependency.
Example (tests/api_usage_tests.rs
):
// Import the library crate being tested
use my_crate; // Use the actual name defined in Cargo.toml
#[test]
fn test_public_api_call() {
// Can only call public items (like process_data) from my_crate
let result = my_crate::process_data(&[1, 2, 3, 4]).unwrap();
assert_eq!(result, "Processed 4 bytes");
// Attempting to call private items results in a compile-time error
// let _ = my_crate::internal_helper(&[1]);
// Error: function `internal_helper` is private
}
#[test]
fn test_empty_data_error_via_public_api() {
let result = my_crate::process_data(&[]);
assert!(result.is_err());
}
Key characteristics of integration tests:
- External Perspective: Integration tests can only access
pub
items (functions, structs, enums, modules) defined in your library crate. They cannot access private implementation details. This ensures they test the library as an external user would. - Separate Crates: Because each file directly in
tests/
is a distinct crate, they are compiled independently. This rigorously tests the public contract but means that sharing setup code or utility functions across multiple integration test files requires specific handling.
Sharing Code Between Integration Tests
When multiple integration test files (e.g., tests/feature_a_tests.rs
, tests/feature_b_tests.rs
) need to share common setup code or utility functions, the recommended approach is to place this shared code into a module within a subdirectory of tests
. For instance, you could create a directory tests/common/
and place your shared module code in a file named tests/common/mod.rs
.
The file name mod.rs
is significant here. Using a mod.rs
file inside a directory (e.g., module_name/mod.rs
) to define the root of a module named module_name
is an established pattern in Rust. While modern Rust (since the 2018 edition) often allows for a module_name.rs
file to implicitly own a same-named directory for its submodules (e.g., src/my_module.rs
and src/my_module/child.rs
), the module_name/mod.rs
convention is crucial when a directory itself is intended to be the module’s primary source, as is common for organizing shared utilities within the tests
directory. When you declare mod common;
in an integration test file (which acts as its own crate, like tests/api_usage_tests.rs
), and common
corresponds to a directory (in this case, tests/common/
), Rust specifically expects to find tests/common/mod.rs
. This mod.rs
file serves as the root or entry point of the common
module.
Files residing within such subdirectories (like tests/common/mod.rs
) are not automatically compiled as separate test crates by Cargo.
If you were to name the file differently, for example, tests/common/my_utils.rs
, a simple mod common;
declaration would not load it as the root of the common
module. tests/common/my_utils.rs
could, however, be a submodule if tests/common/mod.rs
declared it (e.g., pub mod my_utils;
).
Example Structure with Shared Code:
(Refer to the directory structure shown at the beginning of section 24.3.2, which includes tests/common/mod.rs
)
Shared Utilities (tests/common/mod.rs
):
// tests/common/mod.rs
// This file defines the 'common' module's contents because it is named 'mod.rs'
// within the 'common/' directory. It is not compiled as a separate test crate
// due to its location in a subdirectory.
pub fn setup_environment() {
// ... perform common setup actions ...
println!("Common setup for an integration test performed.");
}
pub fn create_sample_data() -> Vec<u8> {
vec![10, 20, 30]
}
// If you had other utility files within tests/common/, for example,
// tests/common/internal_helpers.rs, you would declare them here as submodules:
// pub mod internal_helpers; // This would make common::internal_helpers available.
Using Shared Utilities (e.g., in tests/another_feature_tests.rs
):
// tests/another_feature_tests.rs
// This file IS compiled as a separate test crate.
use my_crate; // Assuming 'my_crate' is the library being tested
// Declare the 'common' module. Because 'common' is a directory,
// Rust resolves this to tests/common/mod.rs.
mod common;
#[test]
fn test_another_feature_with_shared_utils() {
common::setup_environment();
let data = common::create_sample_data();
let result = my_crate::process_data(&data).unwrap();
// Assuming process_data from your lib
assert!(result.contains("3 bytes")); // Adjust assertion as per actual logic
}
By adhering to the tests/common/mod.rs
naming convention, you create a clearly defined common
module accessible to all your integration test files, without Cargo treating the shared code as an independent test suite.
Regarding a tests/common.rs
file (Not a Subdirectory):
If you were to create tests/common.rs
(i.e., a file named common.rs
directly within the tests/
directory, not in a common/
subdirectory), Cargo would treat this tests/common.rs
file as a separate test crate. If it contained no #[test]
functions, it would simply appear as an empty test suite in your test output (e.g., “running 0 tests” for common
). While other test files like tests/api_usage_tests.rs
could still load its contents using mod common;
, this approach is generally less clean as it introduces an unnecessary test target in Cargo’s view. The subdirectory method (tests/common/mod.rs
) is preferred for clarity and to avoid this.
Integration Tests for Binary Crates
Integration tests are primarily designed for library crates (--lib
). If your project is a binary crate (src/main.rs
only), the tests/
directory cannot directly call functions within src/main.rs
because a binary doesn’t produce a linkable artifact in the same way a library does that other crates can depend on for testing.
The recommended approach for testing binary applications is to structure the project with a library component:
- Extract the core logic from
src/main.rs
intosrc/lib.rs
, exposing public functions. This turns your project into a crate that has both a library and a binary. - Keep
src/main.rs
minimal. Its main responsibilities would be parsing command-line arguments and then calling the public functions exposed bysrc/lib.rs
. - Write integration tests in the
tests/
directory that target the public API defined insrc/lib.rs
.
This structure allows the core application logic in src/lib.rs
to be thoroughly tested independently of the command-line interface specifics in src/main.rs
.