Testing in Rust: Frameworks and Tools for Quality Code

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Testing in Rust: from unit tests to property-based testing



Rust is one of the few languages where the type and ownership system eliminates entire classes of errors (data races, dangling pointers, double free) at compile time. However, logical errors, incorrect handling of edge cases, and wrong business logic remain. To detect them, the Rust ecosystem has developed a powerful set of testing tools — from the built-in test runner to advanced frameworks for generating test data.



Built-in tools: cargo test



The basic testing tool is built directly into the compiler and package manager. To create tests, simply add a module with the #[cfg(test)] attribute and mark functions with the #[test] attribute.



// src/lib.rspub fn add(left: u64, right: u64) -> u64 {    left + right}

#[cfg(test)]mod tests { use super::*;

#[test] fn test_add() { let result = add(2, 2); assert_eq!(result, 4); }

#[test] fn test_add_negative() { // u64 does not support negative numbers — this is a compilation error // But if the function accepted i64: // assert_eq!(add(-2, 2), 0); }}


Run: cargo test. The -- --nocapture flag outputs println! from tests. The -- --test-threads=1 flag disables parallel execution.



Integration tests



Integration tests are located in the tests/ folder at the project root level. Each file is a separate crate that can test the public API of the library.



// tests/integration_test.rsuse my_library::add;

#[test]fn test_integration() { assert_eq!(add(10, 20), 30);}


For testing binary crates (with main.rs), you can use assert_cmd — it runs the binary as an external process and checks stdout/stderr.



Property-based testing: proptest and quickcheck



Instead of manually writing dozens of test cases, property-based testing generates thousands of random input data and checks invariants. Two main frameworks:



  • proptest — modern, flexible, with support for strategies, filtering, and example shrinking.
  • quickcheck — older, inspired by Haskell QuickCheck, simpler but less powerful.


Example with proptest:



use proptest::prelude::*;

fn reverse_string(s: &str) -> String { s.chars().rev().collect()}

proptest! { #[test] fn test_reverse_twice(s in ".*") { let reversed = reverse_string(&s); let double_reversed = reverse_string(&reversed); assert_eq!(s, double_reversed); }

#[test] fn test_reverse_length(s in ".*") { let reversed = reverse_string(&s); assert_eq!(s.len(), reversed.len()); }}


If a test fails, proptest finds the minimal counterexample (shrinking).



Testing asynchronous code: tokio-test



For asynchronous functions working with tokio, regular tests are not suitable — an async runtime is needed. tokio-test provides the #[tokio::test] macro.



use tokio::time::{sleep, Duration};

async fn delayed_add(a: u32, b: u32) -> u32 { sleep(Duration::from_millis(10)).await; a + b}

#[tokio::test]async fn test_delayed_add() { let result = delayed_add(3, 5).await; assert_eq!(result, 8);}


You can also use futures-test for working with futures and async-std.



Mocks and stubs: mockall and mockito



To isolate tests from external dependencies (DB, API, file system)

Recommendations