Comprehensive guide to Rust automated testing covering unit, integration, and documentation tests...
Unit Tests: Use #[cfg(test)] modules within the same file
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_functionality() {
assert_eq!(2 + 2, 4);
}
}
Integration Tests: Place in tests/ directory to test public APIs
Documentation Tests: Embed examples in doc comments that double as tests
/// Adds two numbers together.
///
/// ```
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
Use Descriptive Test Names: Clearly express test intent
#[test]
fn connection_timeout_returns_error_after_30_seconds() { }
Follow AAA Pattern: Arrange (setup), Act (execute), Assert (verify)
Test Boundary Conditions: Empty inputs, maximum values, error cases, edge scenarios
One Assertion Per Test: Focus each test on a single behavior (when practical)
Test Panics with #[should_panic]:
#[test]
#[should_panic(expected = "invalid input")]
fn test_panics_on_invalid_input() {
process_data(""); // Should panic
}
Async Testing: Use tokio::test or async-std::test
#[tokio::test]
async fn test_async_operation() {
let result = fetch_data().await;
assert!(result.is_ok());
}
Property-Based Testing: Use proptest to test invariants across random inputs
proptest! {
#[test]
fn reversing_twice_returns_original(s: String) {
let reversed_twice = s.chars().rev().collect::<String>()
.chars().rev().collect::<String>();
assert_eq!(s, reversed_twice);
}
}
Important: Before using mockall, always read the latest official documentation at https://docs.rs/mockall for up-to-date API and best practices.
Mock objects serve two main purposes:
Key Principle: Mock the trait boundaries (interfaces/dependencies), not concrete implementations.
Add to Cargo.toml:
[dev-dependencies]
mockall = "0.14"
Using #[automock] (from official repository):
#[cfg(test)]
use mockall::{automock, mock, predicate::*};
#[cfg_attr(test, automock)]
trait MyTrait {
fn foo(&self, x: u32) -> u32;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mytest() {
let mut mock = MockMyTrait::new();
mock.expect_foo()
.with(eq(4)) // Argument matcher
.times(1) // Expected call count
.returning(|x| x + 1); // Return value
assert_eq!(5, mock.foo(4));
}
}
#[automock]
trait MyTrait {
fn foo(&self) -> u32;
fn bar(&self, x: u32, y: u32) -> u32;
}
let mut mock = MockMyTrait::new();
mock.expect_foo()
.return_const(42u32); // Constant value
mock.expect_bar()
.returning(|x, y| x + y); // Computed value
#[automock]
trait Foo {
fn foo(&self, x: u32) -> u32;
}
let mut mock = MockFoo::new();
mock.expect_foo()
.with(eq(5))
.return_const(50u32);
mock.expect_foo()
.with(eq(6))
.return_const(60u32);
#[automock]
trait Foo {
fn foo(&self);
}
let mut seq = Sequence::new();
let mut mock1 = MockFoo::new();
mock1.expect_foo()
.times(1)
.in_sequence(&mut seq)
.returning(|| ());
let mut mock2 = MockFoo::new();
mock2.expect_foo()
.times(1)
.in_sequence(&mut seq)
.returning(|| ());
--testcargo watch -x test)Run specific tests:
cargo test test_name
cargo test module_name::
cargo test --test integration_test_name
Run with output:
cargo test -- --nocapture # Show println! output
cargo test -- --show-output # Show output for passing tests
Parallel vs Sequential:
cargo test -- --test-threads=1 # Run serially
Set up GitHub Actions to:
cargo test --all-featurescargo clippy and cargo fmt --checkExample workflow snippet:
- name: Run tests
run: |
cargo test --all-features --workspace
cargo test --doc
Use criterion for reliable benchmarks:
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn benchmark_function(c: &mut Criterion) {
c.bench_function("my_function", |b| {
b.iter(|| my_function(black_box(100)))
});
}
criterion_group!(benches, benchmark_function);
criterion_main!(benches);
Keep Tests FIRST:
Maintain Test Code Quality: Apply same standards as production code—tests should be clean, readable, and maintainable.
Avoid Test Flakiness: Don't use sleep(), random values without seeds, or depend on external state.
Test Fixtures: Use setup() helper functions or the rstest crate for parameterized tests
Custom Assertions: Create helper functions for complex assertions
Error Testing: Test both error types and error messages
#[test]
fn returns_correct_error() {
let result = fallible_operation();
assert!(matches!(result, Err(Error::InvalidInput)));
}
Key Takeaway: Comprehensive testing is a cornerstone of reliable Rust software. Invest in your test suite—it pays dividends in confidence, maintainability, and rapid development.