Rust Testing Best Practices¶
Objective: Master senior-level Rust testing patterns for production systems. When you need to build comprehensive test suites, when you want to ensure code reliability, when you need enterprise-grade testing strategiesโthese best practices become your weapon of choice.
Core Principles¶
- Test Coverage: Aim for high test coverage with meaningful tests
- Test Isolation: Each test should be independent and isolated
- Property-Based Testing: Use property-based testing for complex logic
- Performance Testing: Include benchmarks and performance tests
- Integration Testing: Test complete workflows and interactions
Unit Testing Patterns¶
Basic Unit Testing¶
// rust/01-unit-testing.rs
/*
Unit testing patterns and best practices for Rust
*/
use std::collections::HashMap;
use anyhow::Result;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CalculatorError {
#[error("Division by zero")]
DivisionByZero,
#[error("Invalid operation: {0}")]
InvalidOperation(String),
}
pub struct Calculator {
history: Vec<String>,
}
impl Calculator {
pub fn new() -> Self {
Self {
history: Vec::new(),
}
}
pub fn add(&mut self, a: f64, b: f64) -> f64 {
let result = a + b;
self.history.push(format!("{} + {} = {}", a, b, result));
result
}
pub fn subtract(&mut self, a: f64, b: f64) -> f64 {
let result = a - b;
self.history.push(format!("{} - {} = {}", a, b, result));
result
}
pub fn multiply(&mut self, a: f64, b: f64) -> f64 {
let result = a * b;
self.history.push(format!("{} * {} = {}", a, b, result));
result
}
pub fn divide(&mut self, a: f64, b: f64) -> Result<f64, CalculatorError> {
if b == 0.0 {
return Err(CalculatorError::DivisionByZero);
}
let result = a / b;
self.history.push(format!("{} / {} = {}", a, b, result));
Ok(result)
}
pub fn get_history(&self) -> &[String] {
&self.history
}
pub fn clear_history(&mut self) {
self.history.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_addition() {
let mut calc = Calculator::new();
assert_eq!(calc.add(2.0, 3.0), 5.0);
assert_eq!(calc.add(-1.0, 1.0), 0.0);
assert_eq!(calc.add(0.0, 0.0), 0.0);
}
#[test]
fn test_subtraction() {
let mut calc = Calculator::new();
assert_eq!(calc.subtract(5.0, 3.0), 2.0);
assert_eq!(calc.subtract(1.0, 1.0), 0.0);
assert_eq!(calc.subtract(0.0, 5.0), -5.0);
}
#[test]
fn test_multiplication() {
let mut calc = Calculator::new();
assert_eq!(calc.multiply(2.0, 3.0), 6.0);
assert_eq!(calc.multiply(-2.0, 3.0), -6.0);
assert_eq!(calc.multiply(0.0, 5.0), 0.0);
}
#[test]
fn test_division_success() {
let mut calc = Calculator::new();
assert_eq!(calc.divide(6.0, 2.0).unwrap(), 3.0);
assert_eq!(calc.divide(5.0, 2.0).unwrap(), 2.5);
assert_eq!(calc.divide(0.0, 5.0).unwrap(), 0.0);
}
#[test]
fn test_division_by_zero() {
let mut calc = Calculator::new();
let result = calc.divide(5.0, 0.0);
assert!(result.is_err());
match result.unwrap_err() {
CalculatorError::DivisionByZero => {},
_ => panic!("Expected DivisionByZero error"),
}
}
#[test]
fn test_history() {
let mut calc = Calculator::new();
calc.add(1.0, 2.0);
calc.subtract(5.0, 3.0);
let history = calc.get_history();
assert_eq!(history.len(), 2);
assert!(history[0].contains("1 + 2 = 3"));
assert!(history[1].contains("5 - 3 = 2"));
}
#[test]
fn test_clear_history() {
let mut calc = Calculator::new();
calc.add(1.0, 2.0);
assert_eq!(calc.get_history().len(), 1);
calc.clear_history();
assert_eq!(calc.get_history().len(), 0);
}
#[test]
fn test_floating_point_precision() {
let mut calc = Calculator::new();
let result = calc.add(0.1, 0.2);
assert!((result - 0.3).abs() < 1e-10);
}
}
// Test with custom test harness
#[cfg(test)]
mod custom_tests {
use super::*;
// Test with setup and teardown
fn setup_calculator() -> Calculator {
let mut calc = Calculator::new();
calc.add(1.0, 2.0); // Pre-populate with some data
calc
}
fn teardown_calculator(calc: &mut Calculator) {
calc.clear_history();
}
#[test]
fn test_with_setup_teardown() {
let mut calc = setup_calculator();
assert_eq!(calc.get_history().len(), 1);
calc.add(3.0, 4.0);
assert_eq!(calc.get_history().len(), 2);
teardown_calculator(&mut calc);
assert_eq!(calc.get_history().len(), 0);
}
}
Property-Based Testing¶
// rust/02-property-based-testing.rs
/*
Property-based testing patterns with proptest
*/
use proptest::prelude::*;
use std::collections::HashMap;
pub struct StringProcessor {
data: HashMap<String, String>,
}
impl StringProcessor {
pub fn new() -> Self {
Self {
data: HashMap::new(),
}
}
pub fn add(&mut self, key: String, value: String) -> Result<(), String> {
if key.is_empty() {
return Err("Key cannot be empty".to_string());
}
if key.len() > 100 {
return Err("Key too long".to_string());
}
if value.len() > 1000 {
return Err("Value too long".to_string());
}
self.data.insert(key, value);
Ok(())
}
pub fn get(&self, key: &str) -> Option<&String> {
self.data.get(key)
}
pub fn remove(&mut self, key: &str) -> Option<String> {
self.data.remove(key)
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn contains_key(&self, key: &str) -> bool {
self.data.contains_key(key)
}
}
#[cfg(test)]
mod property_tests {
use super::*;
proptest! {
#[test]
fn test_add_get_property(
key in "[a-zA-Z0-9_]{1,50}",
value in ".*"
) {
let mut processor = StringProcessor::new();
processor.add(key.clone(), value.clone()).unwrap();
assert_eq!(processor.get(&key), Some(&value));
}
#[test]
fn test_add_remove_property(
key in "[a-zA-Z0-9_]{1,50}",
value in ".*"
) {
let mut processor = StringProcessor::new();
processor.add(key.clone(), value.clone()).unwrap();
assert_eq!(processor.remove(&key), Some(value));
assert_eq!(processor.get(&key), None);
}
#[test]
fn test_length_property(
operations in prop::collection::vec(
(any::<String>(), any::<String>()),
0..100
)
) {
let mut processor = StringProcessor::new();
let mut expected_len = 0;
for (key, value) in operations {
if !key.is_empty() && key.len() <= 100 && value.len() <= 1000 {
if processor.add(key.clone(), value).is_ok() {
expected_len += 1;
}
}
}
assert_eq!(processor.len(), expected_len);
}
#[test]
fn test_empty_key_error(
value in ".*"
) {
let mut processor = StringProcessor::new();
let result = processor.add("".to_string(), value);
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty"));
}
#[test]
fn test_key_too_long(
key in "[a-zA-Z0-9_]{101,200}",
value in ".*"
) {
let mut processor = StringProcessor::new();
let result = processor.add(key, value);
assert!(result.is_err());
assert!(result.unwrap_err().contains("too long"));
}
#[test]
fn test_value_too_long(
key in "[a-zA-Z0-9_]{1,50}",
value in "[a-zA-Z0-9_]{1001,2000}"
) {
let mut processor = StringProcessor::new();
let result = processor.add(key, value);
assert!(result.is_err());
assert!(result.unwrap_err().contains("too long"));
}
}
}
// Custom property-based test strategies
#[cfg(test)]
mod custom_property_tests {
use super::*;
use proptest::strategy::{Strategy, ValueTree};
use proptest::test_runner::TestRunner;
// Custom strategy for generating valid keys
fn valid_key_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z0-9_]{1,50}".prop_map(|s| s)
}
// Custom strategy for generating valid values
fn valid_value_strategy() -> impl Strategy<Value = String> {
".*".prop_filter("value length", |s| s.len() <= 1000)
}
proptest! {
#[test]
fn test_custom_strategy(
key in valid_key_strategy(),
value in valid_value_strategy()
) {
let mut processor = StringProcessor::new();
assert!(processor.add(key.clone(), value.clone()).is_ok());
assert_eq!(processor.get(&key), Some(&value));
}
}
}
Mock Testing¶
// rust/03-mock-testing.rs
/*
Mock testing patterns with mockall
*/
use mockall::mock;
use std::collections::HashMap;
use anyhow::Result;
// Trait to mock
pub trait DataRepository {
fn get(&self, key: &str) -> Option<String>;
fn set(&mut self, key: String, value: String) -> Result<()>;
fn delete(&mut self, key: &str) -> Result<()>;
fn exists(&self, key: &str) -> bool;
}
// Implementation
pub struct HashMapRepository {
data: HashMap<String, String>,
}
impl HashMapRepository {
pub fn new() -> Self {
Self {
data: HashMap::new(),
}
}
}
impl DataRepository for HashMapRepository {
fn get(&self, key: &str) -> Option<String> {
self.data.get(key).cloned()
}
fn set(&mut self, key: String, value: String) -> Result<()> {
self.data.insert(key, value);
Ok(())
}
fn delete(&mut self, key: &str) -> Result<()> {
self.data.remove(key);
Ok(())
}
fn exists(&self, key: &str) -> bool {
self.data.contains_key(key)
}
}
// Service that uses the repository
pub struct DataService {
repository: Box<dyn DataRepository>,
}
impl DataService {
pub fn new(repository: Box<dyn DataRepository>) -> Self {
Self { repository }
}
pub fn get_data(&self, key: &str) -> Option<String> {
self.repository.get(key)
}
pub fn set_data(&mut self, key: String, value: String) -> Result<()> {
self.repository.set(key, value)
}
pub fn delete_data(&mut self, key: &str) -> Result<()> {
self.repository.delete(key)
}
pub fn data_exists(&self, key: &str) -> bool {
self.repository.exists(key)
}
pub fn get_or_set_default(&mut self, key: String, default_value: String) -> Result<String> {
if let Some(value) = self.repository.get(&key) {
Ok(value)
} else {
self.repository.set(key.clone(), default_value.clone())?;
Ok(default_value)
}
}
}
// Mock the trait
mock! {
pub DataRepository {}
impl DataRepository for DataRepository {
fn get(&self, key: &str) -> Option<String>;
fn set(&mut self, key: String, value: String) -> Result<()>;
fn delete(&mut self, key: &str) -> Result<()>;
fn exists(&self, key: &str) -> bool;
}
}
#[cfg(test)]
mod mock_tests {
use super::*;
use mockall::predicate::*;
#[test]
fn test_get_data_success() {
let mut mock_repo = MockDataRepository::new();
mock_repo
.expect_get()
.with(eq("test_key"))
.times(1)
.returning(|_| Some("test_value".to_string()));
let service = DataService::new(Box::new(mock_repo));
let result = service.get_data("test_key");
assert_eq!(result, Some("test_value".to_string()));
}
#[test]
fn test_get_data_not_found() {
let mut mock_repo = MockDataRepository::new();
mock_repo
.expect_get()
.with(eq("nonexistent_key"))
.times(1)
.returning(|_| None);
let service = DataService::new(Box::new(mock_repo));
let result = service.get_data("nonexistent_key");
assert_eq!(result, None);
}
#[test]
fn test_set_data_success() {
let mut mock_repo = MockDataRepository::new();
mock_repo
.expect_set()
.with(eq("test_key".to_string()), eq("test_value".to_string()))
.times(1)
.returning(|_, _| Ok(()));
let mut service = DataService::new(Box::new(mock_repo));
let result = service.set_data("test_key".to_string(), "test_value".to_string());
assert!(result.is_ok());
}
#[test]
fn test_delete_data_success() {
let mut mock_repo = MockDataRepository::new();
mock_repo
.expect_delete()
.with(eq("test_key"))
.times(1)
.returning(|_| Ok(()));
let mut service = DataService::new(Box::new(mock_repo));
let result = service.delete_data("test_key");
assert!(result.is_ok());
}
#[test]
fn test_data_exists_true() {
let mut mock_repo = MockDataRepository::new();
mock_repo
.expect_exists()
.with(eq("test_key"))
.times(1)
.returning(|_| true);
let service = DataService::new(Box::new(mock_repo));
let result = service.data_exists("test_key");
assert!(result);
}
#[test]
fn test_get_or_set_default_existing() {
let mut mock_repo = MockDataRepository::new();
mock_repo
.expect_get()
.with(eq("existing_key"))
.times(1)
.returning(|_| Some("existing_value".to_string()));
let mut service = DataService::new(Box::new(mock_repo));
let result = service.get_or_set_default("existing_key".to_string(), "default_value".to_string()).unwrap();
assert_eq!(result, "existing_value");
}
#[test]
fn test_get_or_set_default_missing() {
let mut mock_repo = MockDataRepository::new();
mock_repo
.expect_get()
.with(eq("missing_key"))
.times(1)
.returning(|_| None);
mock_repo
.expect_set()
.with(eq("missing_key".to_string()), eq("default_value".to_string()))
.times(1)
.returning(|_, _| Ok(()));
let mut service = DataService::new(Box::new(mock_repo));
let result = service.get_or_set_default("missing_key".to_string(), "default_value".to_string()).unwrap();
assert_eq!(result, "default_value");
}
}
Integration Testing¶
// rust/04-integration-testing.rs
/*
Integration testing patterns for Rust applications
*/
use std::collections::HashMap;
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: u64,
pub name: String,
pub email: String,
pub age: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserUpdate {
pub name: Option<String>,
pub email: Option<String>,
pub age: Option<u32>,
}
pub struct UserService {
users: HashMap<u64, User>,
next_id: u64,
}
impl UserService {
pub fn new() -> Self {
Self {
users: HashMap::new(),
next_id: 1,
}
}
pub fn create_user(&mut self, name: String, email: String, age: u32) -> Result<User> {
if name.is_empty() {
return Err(anyhow::anyhow!("Name cannot be empty"));
}
if email.is_empty() {
return Err(anyhow::anyhow!("Email cannot be empty"));
}
if age > 150 {
return Err(anyhow::anyhow!("Age must be less than 150"));
}
let user = User {
id: self.next_id,
name,
email,
age,
};
self.users.insert(user.id, user.clone());
self.next_id += 1;
Ok(user)
}
pub fn get_user(&self, id: u64) -> Option<&User> {
self.users.get(&id)
}
pub fn update_user(&mut self, id: u64, update: UserUpdate) -> Result<User> {
let user = self.users.get_mut(&id)
.ok_or_else(|| anyhow::anyhow!("User not found"))?;
if let Some(name) = update.name {
if name.is_empty() {
return Err(anyhow::anyhow!("Name cannot be empty"));
}
user.name = name;
}
if let Some(email) = update.email {
if email.is_empty() {
return Err(anyhow::anyhow!("Email cannot be empty"));
}
user.email = email;
}
if let Some(age) = update.age {
if age > 150 {
return Err(anyhow::anyhow!("Age must be less than 150"));
}
user.age = age;
}
Ok(user.clone())
}
pub fn delete_user(&mut self, id: u64) -> Result<User> {
self.users.remove(&id)
.ok_or_else(|| anyhow::anyhow!("User not found"))
}
pub fn list_users(&self) -> Vec<&User> {
self.users.values().collect()
}
pub fn search_users(&self, query: &str) -> Vec<&User> {
self.users.values()
.filter(|user| user.name.contains(query) || user.email.contains(query))
.collect()
}
}
// Integration tests
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_full_user_lifecycle() {
let mut service = UserService::new();
// Create user
let user = service.create_user(
"John Doe".to_string(),
"john@example.com".to_string(),
30
).unwrap();
assert_eq!(user.id, 1);
assert_eq!(user.name, "John Doe");
assert_eq!(user.email, "john@example.com");
assert_eq!(user.age, 30);
// Get user
let retrieved_user = service.get_user(user.id).unwrap();
assert_eq!(retrieved_user.name, "John Doe");
// Update user
let update = UserUpdate {
name: Some("John Smith".to_string()),
email: None,
age: Some(31),
};
let updated_user = service.update_user(user.id, update).unwrap();
assert_eq!(updated_user.name, "John Smith");
assert_eq!(updated_user.age, 31);
assert_eq!(updated_user.email, "john@example.com"); // Unchanged
// Search users
let search_results = service.search_users("John");
assert_eq!(search_results.len(), 1);
assert_eq!(search_results[0].name, "John Smith");
// Delete user
let deleted_user = service.delete_user(user.id).unwrap();
assert_eq!(deleted_user.name, "John Smith");
// Verify deletion
assert!(service.get_user(user.id).is_none());
}
#[test]
fn test_multiple_users() {
let mut service = UserService::new();
// Create multiple users
let user1 = service.create_user("Alice".to_string(), "alice@example.com".to_string(), 25).unwrap();
let user2 = service.create_user("Bob".to_string(), "bob@example.com".to_string(), 30).unwrap();
let user3 = service.create_user("Charlie".to_string(), "charlie@example.com".to_string(), 35).unwrap();
// List all users
let all_users = service.list_users();
assert_eq!(all_users.len(), 3);
// Search users
let search_results = service.search_users("example.com");
assert_eq!(search_results.len(), 3);
// Update one user
let update = UserUpdate {
name: Some("Alice Smith".to_string()),
email: None,
age: None,
};
service.update_user(user1.id, update).unwrap();
// Verify update
let updated_user = service.get_user(user1.id).unwrap();
assert_eq!(updated_user.name, "Alice Smith");
// Delete one user
service.delete_user(user2.id).unwrap();
// Verify deletion
let remaining_users = service.list_users();
assert_eq!(remaining_users.len(), 2);
assert!(service.get_user(user2.id).is_none());
}
#[test]
fn test_error_handling() {
let mut service = UserService::new();
// Test empty name
let result = service.create_user("".to_string(), "test@example.com".to_string(), 25);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Name cannot be empty"));
// Test empty email
let result = service.create_user("Test".to_string(), "".to_string(), 25);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Email cannot be empty"));
// Test invalid age
let result = service.create_user("Test".to_string(), "test@example.com".to_string(), 200);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Age must be less than 150"));
// Test updating non-existent user
let update = UserUpdate {
name: Some("New Name".to_string()),
email: None,
age: None,
};
let result = service.update_user(999, update);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("User not found"));
// Test deleting non-existent user
let result = service.delete_user(999);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("User not found"));
}
}
TL;DR Runbook¶
Quick Start¶
// 1. Basic unit testing
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_functionality() {
// Test implementation
}
}
// 2. Property-based testing
use proptest::prelude::*;
proptest! {
#[test]
fn test_property(
input in any::<String>()
) {
// Property test implementation
}
}
// 3. Mock testing
use mockall::mock;
mock! {
pub MyTrait {}
impl MyTrait for MyTrait {
fn method(&self) -> String;
}
}
// 4. Integration testing
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_full_workflow() {
// Integration test implementation
}
}
Essential Patterns¶
// Complete testing setup
pub fn setup_rust_testing() {
// 1. Unit testing
// 2. Property-based testing
// 3. Mock testing
// 4. Integration testing
// 5. Performance testing
// 6. Test organization
// 7. Test data management
// 8. Test utilities
println!("Rust testing setup complete!");
}
This guide provides the complete machinery for Rust testing best practices. Each pattern includes implementation examples, testing strategies, and real-world usage patterns for enterprise test development.