Testing software code is one of the oldest and most important processes in the development industry. However, in many languages, testing libraries are provided separately, and integrating tests with the main code can be opaque or cumbersome. In Rust, from its earliest versions, the language was designed with built-in support for explicit modular testing.
In many traditional languages, tests are located separately or require specific frameworks and builders to be set up. This complicates collaborative development, increases the risk of desynchronization between main logic and tests, and makes it harder to perform integration and modular testing.
Rust integrates the testing system directly at the language level: the module #[cfg(test)], the directive #[test], and the tool cargo test allow for creating, running, and managing tests within the source code. This ensures a close relationship between the code and the tests that verify it, guarantees ease of test execution, and automates CI/CD processes.
Code example:
// tests automatically compiled and run by `cargo test` #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(2 + 2, 4); } }
Key features:
cargo test) runs all discovered tests.#[cfg(test)] allows tests not to be included in the release build.Can unstable or private functions be used in tests, and how can this be done?
Yes, they can. Inside the test module declared as #[cfg(test)], tests have access to both public and private API of the current module. This means you can test implementation details directly. However, from another module — only through the public interface.
Example:
fn private_internal(x: i32) -> i32 { x + 1 } #[cfg(test)] mod tests { use super::*; #[test] fn test_private() { assert_eq!(private_internal(41), 42); } }
Will tests with the same names conflict between different modules?
No, test names are only visible within their own module. Tests from different modules can have the same names because the compiler differentiates them by their full path (namespace).
Example:
mod a { #[cfg(test)] mod tests { #[test] fn basic() {} } } mod b { #[cfg(test)] mod tests { #[test] fn basic() {} } }
Can a single test or part of a set of tests be run separately?
Yes, through test name filtering: cargo test test_name. This is convenient for debugging large sets.
Example:
cargo test only_this_test
A developer moved tests to a separate project without access to private functions and with a different directory structure. Due to changes in the main library, tests quickly fell behind and became outdated.
Pros: Tests can be isolated, avoiding accidental inclusion of test modules in the release build.
Cons: Loss of synchronization, inability to test internal details, high maintenance, and risk of test obsolescence.
Tests are written right in the same modules as the main logic, small atomic test cases test the private and public API. A beginner quickly understands the module's workings through tests.
Pros: Maximum support for synchronization, transparency, faster local testing, suitability for test-driven development.
Cons: Increase in lines of code in source files, potential growth in complexity with a large number of tests.