ProgrammingRust Developer / Tester

How does the modular testing system work in Rust, and why are tests closely integrated with the language? How to properly organize tests, ensure their isolation and readability?

Pass interviews with Hintsage AI assistant

Answer.

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:

  • Tests are compiled together with the code and have access to private module functions.
  • Integration tests (tests folder) simulate the library's operation as external.
  • Test isolation by process: each test runs independently and can be parallelized.

Trick questions.

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.

Common mistakes and anti-patterns

  • Using global mutable variables in tests.
  • Non-obvious separation of tests into module tests and integration tests.
  • Lack of negative test scenarios.

Real-life example

Negative case

All tests use a single global variable:

static mut COUNTER: u32 = 0; #[test] fn test_inc() { unsafe { COUNTER += 1; } }

Pros:

  • Simple to implement a counter.

Cons:

  • Data races leading to unpredictable behavior.
  • Tests can accidentally depend on each other.

Positive case

Each test is isolated, uses local variables and mock objects.

Pros:

  • No unexpected interaction effects between tests.
  • Running tests in parallel is safe.

Cons:

  • Requires more careful design of the code under test.
  • Sometimes needs more code to initialize state.