Rust was originally designed with a focus on reliability and safety, which is why modular testing has become an integral part of the language's ecosystem. Tests are integrated at the language and toolchain level (cargo test), making them an organic part of the development process.
Background
In traditional ecosystems (e.g., C/C++, Python, Java), testing existed apart from the program itself. In Rust, tests are part of the code; they are compiled and checked as a full-fledged module. This synergy is achieved thanks to language constructs and compiler features.
Problem
Without proper testing, it is impossible to guarantee the reliability of key functions. For complex and multi-module projects, the question often arises: how to conveniently organize tests without letting them depend on the state of other modules and without complicating the project structure?
Solution
In Rust, tests are placed within the source file itself (using #[cfg(test)]), or in a separate tests folder for integration testing. Each module can have private tests that have access to the private API.
Example code:
pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add_positive() { assert_eq!(add(2, 3), 5); } #[test] fn test_add_negative() { assert_eq!(add(-1, -3), -4); } }
Key features:
What is use super::*; needed for in the test module?
To allow tests to access the functions and structures of the current module (including private ones), use super::*; is typically used in the test.
Can #[test] be asynchronous?
By default, #[test] does not support async, but with external crates (like tokio or async-std), async #[test] can be implemented.
Example code:
#[tokio::test] async fn test_async_add() { assert_eq!(add(2, 2).await, 4); }
Can tests change global state, and how to avoid that?
Tests in Rust are executed in parallel by default, so shared mutable global state (static mut) cannot be used without synchronization; otherwise, data races may occur.
All tests use a single global variable:
static mut COUNTER: u32 = 0; #[test] fn test_inc() { unsafe { COUNTER += 1; } }
Pros:
Cons:
Each test is isolated, uses local variables and mock objects.
Pros:
Cons: