Rust Unit Testing
Unit testing in Rust is a first-class feature with built-in test framework support. Tests verify individual units of code in isolation, helping catch bugs early and document expected behavior. Rust's compiler guarantees test safety with no runtime overhead.
1. Writing and Running Tests
Test Functions: Annotated with #[test], containing assertions.
// In src/lib.rs or a tests/ module
#[cfg(test)] // Compile only when testing
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
#[test]
#[should_panic(expected = "divide by zero")]
fn test_panic() {
divide(10, 0); // Should panic
}
}
// Run tests with:
// cargo test
Key Commands:
cargo test- Runs all testscargo test test_name- Runs specific testcargo test -- --ignored- Runs only ignored tests
2. Test Organization
Module Structure: Tests can live alongside code or in separate files.
// Option 1: Inline tests module
#[cfg(test)]
mod tests {
use super::*; // Import parent module items
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
}
// Option 2: External test file
// tests/integration_test.rs
use my_crate::add;
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
Test Types:
- Unit tests: Test individual functions/modules (
src/) - Integration tests: Test public API (
tests/directory) - Doc tests: Code examples in documentation
3. Assertion Macros
Verification Tools: Rust provides several assertion macros.
#[test]
fn test_assertions() {
// Equality
assert_eq!(2 + 2, 4);
assert_ne!(3, 4);
// Boolean conditions
assert!(result.is_ok());
assert!(!value.is_empty());
// Custom failure messages
assert!(
user.is_admin(),
"User {} is not admin",
user.name
);
// Approximate floating point comparisons
assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10);
}
Common Macros:
assert!- Boolean conditionassert_eq!/assert_ne!- Equality checksassert_matches!- Pattern matching#[should_panic]- Expected failures
4. Test Fixtures
Reusable Setup: Common initialization patterns.
// Setup function
fn setup_test_data() -> Vec<i32> {
vec![1, 2, 3]
}
#[test]
fn test_with_fixture() {
let data = setup_test_data();
assert_eq!(data.len(), 3);
}
// Test-specific modules
mod database_tests {
struct TestDatabase {
conn: DatabaseConnection,
}
impl TestDatabase {
fn new() -> Self {
// Set up test database
}
}
#[test]
fn test_query() {
let db = TestDatabase::new();
// Test with fresh instance
}
}
Advanced Patterns:
Dropimplementations for cleanup- Lazy static initialization
- Mocking with trait objects
5. Advanced Test Features
Special Test Cases: Rust supports various test scenarios.
// 1. Ignored tests
#[test]
#[ignore = "Too slow for CI"]
fn expensive_test() {
// Long-running test
}
// 2. Test return values
#[test]
fn test_result() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("Math is broken"))
}
}
// 3. Benchmark tests (nightly only)
#[bench]
fn bench_sort(b: &mut test::Bencher) {
let mut v = vec![0; 10000];
b.iter(|| v.sort());
}
Test Flags:
--nocapture- Show output from passing tests--test-threads=1- Run tests sequentially--include-ignored- Run all tests including ignored
6. Property-Based Testing
Generate Test Cases: Verify behavior across many inputs.
use proptest::prelude::*;
proptest! {
#[test]
fn test_addition_commutative(a: i32, b: i32) {
assert_eq!(a + b, b + a);
}
#[test]
fn test_string_concat(s in ".*", sub in ".*") {
let combined = format!("{}{}", s, sub);
assert!(combined.contains(&s));
assert!(combined.contains(&sub));
}
}
Property Test Crates:
proptest- General property testingquickcheck- Haskell-style testingrstest- Parameterized tests