Rust Automated Testing Best Practices Guide
1. Test Organization Structure
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 }
2. Core Testing Principles
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)
3. Advanced Testing Techniques
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);
}
}
4. Mocking with mockall
Important: Before using mockall, always read the latest official documentation at https://docs.rs/mockall for up-to-date API and best practices.
When to Use Mocks
Mock objects serve two main purposes:
- Isolation Testing: When testing a specific module, mock the inputs and outputs of other modules to isolate the module under test.
- Simulating Edge Cases: Mock boundary conditions to test scenarios that are difficult to trigger or reproduce in real-world situations.
Key Principle: Mock the trait boundaries (interfaces/dependencies), not concrete implementations.
Basic Usage
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));
}
}
Return Values
#[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
Matching Multiple Calls
#[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);
Sequences (Enforce Call Order)
#[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(|| ());
5. Essential Testing Tools
- Cargo test: Built-in test runner with filtering via
--test - Cargo-nextest: Faster parallel test execution with better output
- Cargo-watch: Auto-run tests on file changes (
cargo watch -x test) - Tarpaulin/llvm-cov: Code coverage analysis
- Criterion: Statistical benchmarking framework
6. Test Execution Strategies
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
7. CI/CD Integration
Set up GitHub Actions to:
- Run tests on multiple Rust versions (stable, beta, nightly)
- Test all feature combinations:
cargo test --all-features - Check code coverage and fail if below threshold
- Run
cargo clippyandcargo fmt --check
Example workflow snippet:
- name: Run tests
run: |
cargo test --all-features --workspace
cargo test --doc
8. Performance and Benchmark Testing
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);
9. Test Quality Guidelines
Keep Tests FIRST:
- Fast: Tests should run quickly
- Isolated: No dependencies between tests
- Repeatable: Same results every time
- Self-validating: Pass or fail, no manual checking
- Timely: Written alongside or before production code
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.
10. Common Patterns
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.
微信扫一扫