Unit Testing
Unit testing in Rust is built into the language and toolchain, making it easy to write, organize, and run tests. Rust's testing framework provides powerful features for ensuring code correctness and maintaining code quality.
Basic Test Structure
Writing Your First Tests
// Basic function to test
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
// Tests module
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
assert_eq!(add(-1, 1), 0);
assert_eq!(add(0, 0), 0);
}
#[test]
fn test_divide_success() {
assert_eq!(divide(10.0, 2.0), Ok(5.0));
assert_eq!(divide(7.0, 2.0), Ok(3.5));
}
#[test]
fn test_divide_by_zero() {
assert_eq!(divide(10.0, 0.0), Err("Division by zero".to_string()));
}
#[test]
#[should_panic]
fn test_panic_behavior() {
panic!("This test should panic");
}
#[test]
#[should_panic(expected = "specific panic message")]
fn test_specific_panic() {
panic!("specific panic message");
}
}
Running Tests
# Run all tests
cargo test
# Run tests with output
cargo test -- --nocapture
# Run specific test
cargo test test_add
# Run tests matching pattern
cargo test divide
# Run tests in single thread (for debugging)
cargo test -- --test-threads=1
# Show test execution time
cargo test -- --show-output
Test Organization
Organizing Tests in Modules
// src/lib.rs
pub mod calculator;
pub mod geometry;
// src/calculator.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_multiply() {
assert_eq!(multiply(3, 4), 12);
}
}
// src/geometry.rs
pub struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
pub fn new(width: u32, height: u32) -> Self {
Rectangle { width, height }
}
pub fn area(&self) -> u32 {
self.width * self.height
}
pub fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_area() {
let rect = Rectangle::new(10, 20);
assert_eq!(rect.area(), 200);
}
#[test]
fn test_can_hold() {
let larger = Rectangle::new(10, 8);
let smaller = Rectangle::new(5, 3);
assert!(larger.can_hold(&smaller));
assert!(!smaller.can_hold(&larger));
}
}
Test-Only Code and Helper Functions
// Helper functions for tests
#[cfg(test)]
mod test_helpers {
use super::*;
pub fn create_test_user(name: &str) -> User {
User {
id: 1,
name: name.to_string(),
email: format!("{}@test.com", name.to_lowercase()),
created_at: chrono::Utc::now(),
}
}
pub fn create_test_data() -> Vec<i32> {
vec![1, 2, 3, 4, 5]
}
// Custom assertion for complex types
pub fn assert_user_equal(actual: &User, expected: &User) {
assert_eq!(actual.id, expected.id);
assert_eq!(actual.name, expected.name);
assert_eq!(actual.email, expected.email);
// Don't compare timestamps exactly due to timing issues
assert!((actual.created_at - expected.created_at).num_seconds() < 1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_helpers::*;
#[test]
fn test_user_creation() {
let user = create_test_user("Alice");
assert_eq!(user.name, "Alice");
assert_eq!(user.email, "[email protected]");
}
#[test]
fn test_with_test_data() {
let data = create_test_data();
assert_eq!(data.len(), 5);
assert_eq!(data[0], 1);
}
}
Assertions and Testing Macros
Built-in Assertion Macros
#[cfg(test)]
mod assertion_tests {
#[test]
fn test_basic_assertions() {
// Equality assertions
assert_eq!(2 + 2, 4);
assert_ne!(2 + 2, 5);
// Boolean assertions
assert!(true);
assert!(!false);
// Custom messages
assert_eq!(2 + 2, 4, "Math is broken!");
assert!(true, "This should never fail");
}
#[test]
fn test_floating_point() {
let a = 0.1 + 0.2;
let b = 0.3;
// Don't do this - floating point comparison
// assert_eq!(a, b);
// Do this instead
assert!((a - b).abs() < f64::EPSILON);
// Or use approx crate
// assert_relative_eq!(a, b);
}
#[test]
fn test_collections() {
let vec1 = vec![1, 2, 3];
let vec2 = vec![1, 2, 3];
let vec3 = vec![3, 2, 1];
assert_eq!(vec1, vec2);
assert_ne!(vec1, vec3);
// Testing if collection contains element
assert!(vec1.contains(&2));
assert!(!vec1.contains(&5));
// Testing collection properties
assert!(!vec1.is_empty());
assert_eq!(vec1.len(), 3);
}
#[test]
fn test_option_and_result() {
let some_value: Option<i32> = Some(42);
let none_value: Option<i32> = None;
assert!(some_value.is_some());
assert!(none_value.is_none());
assert_eq!(some_value.unwrap(), 42);
let ok_result: Result<i32, &str> = Ok(42);
let err_result: Result<i32, &str> = Err("error");
assert!(ok_result.is_ok());
assert!(err_result.is_err());
assert_eq!(ok_result.unwrap(), 42);
assert_eq!(err_result.unwrap_err(), "error");
}
#[test]
fn test_string_assertions() {
let text = "Hello, World!";
assert!(text.starts_with("Hello"));
assert!(text.ends_with("World!"));
assert!(text.contains("World"));
assert_eq!(text.len(), 13);
let string = String::from("test");
assert_eq!(string, "test");
assert_eq!(string.as_str(), "test");
}
}
Custom Assertion Macros
// Custom assertion macro for ranges
macro_rules! assert_in_range {
($value:expr, $min:expr, $max:expr) => {
assert!(
$value >= $min && $value <= $max,
"Value {} is not in range [{}, {}]",
$value, $min, $max
);
};
}
// Custom assertion for approximately equal
macro_rules! assert_approx_eq {
($left:expr, $right:expr, $epsilon:expr) => {
assert!(
($left - $right).abs() < $epsilon,
"Values {} and {} are not approximately equal (epsilon: {})",
$left, $right, $epsilon
);
};
}
#[cfg(test)]
mod custom_assertion_tests {
#[test]
fn test_custom_assertions() {
assert_in_range!(5, 1, 10);
assert_approx_eq!(0.1 + 0.2, 0.3, 0.0001);
}
#[test]
#[should_panic(expected = "not in range")]
fn test_range_assertion_failure() {
assert_in_range!(15, 1, 10);
}
}
// More sophisticated custom assertions
pub struct TestAssertions;
impl TestAssertions {
pub fn assert_collections_equivalent<T: PartialEq + std::fmt::Debug>(
actual: &[T],
expected: &[T],
) {
assert_eq!(
actual.len(),
expected.len(),
"Collections have different lengths. Actual: {:?}, Expected: {:?}",
actual, expected
);
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
assert_eq!(
a, e,
"Elements at index {} differ. Actual: {:?}, Expected: {:?}",
i, a, e
);
}
}
pub fn assert_sorted<T: PartialOrd + std::fmt::Debug>(slice: &[T]) {
for window in slice.windows(2) {
assert!(
window[0] <= window[1],
"Slice is not sorted. Found {:?} > {:?}",
window[0], window[1]
);
}
}
}
#[cfg(test)]
mod advanced_assertion_tests {
use super::*;
#[test]
fn test_collection_equivalence() {
let actual = vec![1, 2, 3, 4];
let expected = vec![1, 2, 3, 4];
TestAssertions::assert_collections_equivalent(&actual, &expected);
}
#[test]
fn test_sorted_assertion() {
let sorted = vec![1, 2, 3, 4, 5];
TestAssertions::assert_sorted(&sorted);
}
}
Testing Error Handling
Testing Results and Errors
#[derive(Debug, PartialEq)]
pub enum MathError {
DivisionByZero,
NegativeSquareRoot,
Overflow,
}
pub fn safe_divide(a: f64, b: f64) -> Result<f64, MathError> {
if b == 0.0 {
Err(MathError::DivisionByZero)
} else {
Ok(a / b)
}
}
pub fn square_root(x: f64) -> Result<f64, MathError> {
if x < 0.0 {
Err(MathError::NegativeSquareRoot)
} else {
Ok(x.sqrt())
}
}
pub fn factorial(n: u32) -> Result<u64, MathError> {
if n > 20 {
return Err(MathError::Overflow);
}
let mut result = 1u64;
for i in 1..=n {
result *= i as u64;
}
Ok(result)
}
#[cfg(test)]
mod error_tests {
use super::*;
#[test]
fn test_successful_operations() {
assert_eq!(safe_divide(10.0, 2.0), Ok(5.0));
assert_eq!(square_root(9.0), Ok(3.0));
assert_eq!(factorial(5), Ok(120));
}
#[test]
fn test_error_conditions() {
assert_eq!(safe_divide(10.0, 0.0), Err(MathError::DivisionByZero));
assert_eq!(square_root(-1.0), Err(MathError::NegativeSquareRoot));
assert_eq!(factorial(25), Err(MathError::Overflow));
}
#[test]
fn test_error_handling_patterns() {
match safe_divide(10.0, 0.0) {
Ok(_) => panic!("Expected error but got success"),
Err(MathError::DivisionByZero) => {
// Expected error
}
Err(other) => panic!("Unexpected error: {:?}", other),
}
}
#[test]
fn test_chained_operations() {
let result = safe_divide(10.0, 2.0)
.and_then(|x| square_root(x))
.and_then(|x| factorial(x as u32));
assert!(result.is_ok());
assert_eq!(result.unwrap(), 6); // factorial(sqrt(5.0)) = factorial(2) = 2
}
#[test]
fn test_error_propagation() {
fn complex_calculation(a: f64, b: f64) -> Result<u64, MathError> {
let divided = safe_divide(a, b)?;
let sqrt_result = square_root(divided)?;
factorial(sqrt_result as u32)
}
// Test successful case
assert_eq!(complex_calculation(16.0, 4.0), Ok(6)); // 16/4 = 4, sqrt(4) = 2, 2! = 2
// Test error propagation
assert_eq!(complex_calculation(10.0, 0.0), Err(MathError::DivisionByZero));
assert_eq!(complex_calculation(-4.0, 2.0), Err(MathError::NegativeSquareRoot));
}
}
Testing Panics
pub fn divide_integers(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Cannot divide by zero!");
}
a / b
}
pub fn get_element(vec: &[i32], index: usize) -> i32 {
if index >= vec.len() {
panic!("Index {} out of bounds for vector of length {}", index, vec.len());
}
vec[index]
}
#[cfg(test)]
mod panic_tests {
use super::*;
#[test]
#[should_panic]
fn test_divide_by_zero_panics() {
divide_integers(10, 0);
}
#[test]
#[should_panic(expected = "Cannot divide by zero")]
fn test_specific_panic_message() {
divide_integers(10, 0);
}
#[test]
#[should_panic(expected = "out of bounds")]
fn test_index_out_of_bounds() {
let vec = vec![1, 2, 3];
get_element(&vec, 5);
}
#[test]
fn test_catch_unwind() {
use std::panic;
let result = panic::catch_unwind(|| {
divide_integers(10, 0)
});
assert!(result.is_err());
}
// Testing that something doesn't panic
#[test]
fn test_no_panic() {
let result = divide_integers(10, 2);
assert_eq!(result, 5);
}
}
Property-Based Testing with QuickCheck
Basic Property-Based Tests
Add to Cargo.toml
:
[dev-dependencies]
quickcheck = "1.0"
quickcheck_macros = "1.0"
#[cfg(test)]
mod property_tests {
use quickcheck::{quickcheck, TestResult, Arbitrary};
use quickcheck_macros::quickcheck;
// Property: addition is commutative
#[quickcheck]
fn prop_addition_commutative(a: i32, b: i32) -> bool {
a + b == b + a
}
// Property: reversing twice gives original
#[quickcheck]
fn prop_reverse_twice(vec: Vec<i32>) -> bool {
let mut v = vec.clone();
v.reverse();
v.reverse();
v == vec
}
// Property with preconditions
#[quickcheck]
fn prop_division_multiplication(a: f64, b: f64) -> TestResult {
if b == 0.0 || b.is_nan() || b.is_infinite() {
return TestResult::discard();
}
let divided = a / b;
let result = divided * b;
TestResult::from_bool((result - a).abs() < 1e-10)
}
// Property for sorting
#[quickcheck]
fn prop_sorting_preserves_length(mut vec: Vec<i32>) -> bool {
let original_len = vec.len();
vec.sort();
vec.len() == original_len
}
#[quickcheck]
fn prop_sorting_is_sorted(mut vec: Vec<i32>) -> bool {
vec.sort();
vec.windows(2).all(|window| window[0] <= window[1])
}
#[quickcheck]
fn prop_sorting_contains_same_elements(vec: Vec<i32>) -> bool {
let mut sorted = vec.clone();
sorted.sort();
// Check that all elements from original are in sorted
vec.iter().all(|&x| sorted.contains(&x)) &&
// Check that sorted doesn't have extra elements
sorted.iter().all(|&x| vec.contains(&x))
}
// Custom generator for domain-specific data
#[derive(Clone, Debug)]
struct Email(String);
impl Arbitrary for Email {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
let local_part: String = (0..10)
.map(|_| g.choose(&b"abcdefghijklmnopqrstuvwxyz"[..]).unwrap())
.map(|&c| c as char)
.collect();
let domain = g.choose(&["gmail.com", "yahoo.com", "example.org"]).unwrap();
Email(format!("{}@{}", local_part, domain))
}
}
#[quickcheck]
fn prop_email_contains_at(email: Email) -> bool {
email.0.contains('@')
}
}
Advanced Property-Based Testing
#[cfg(test)]
mod advanced_property_tests {
use quickcheck::{quickcheck, TestResult, Gen, Arbitrary};
use std::collections::HashMap;
// Testing a simple key-value store
#[derive(Clone, Debug)]
enum StoreOperation {
Insert(String, i32),
Remove(String),
Get(String),
}
impl Arbitrary for StoreOperation {
fn arbitrary(g: &mut Gen) -> Self {
let keys = ["a", "b", "c", "d", "e"];
let key = g.choose(&keys).unwrap().to_string();
match g.choose(&[0, 1, 2]).unwrap() {
0 => StoreOperation::Insert(key.clone(), Arbitrary::arbitrary(g)),
1 => StoreOperation::Remove(key),
_ => StoreOperation::Get(key),
}
}
}
fn apply_operations(operations: Vec<StoreOperation>) -> HashMap<String, i32> {
let mut store = HashMap::new();
for op in operations {
match op {
StoreOperation::Insert(key, value) => {
store.insert(key, value);
}
StoreOperation::Remove(key) => {
store.remove(&key);
}
StoreOperation::Get(_) => {
// Read operation, doesn't change state
}
}
}
store
}
#[quickcheck]
fn prop_insert_then_contains(key: String, value: i32) -> bool {
let ops = vec![StoreOperation::Insert(key.clone(), value)];
let store = apply_operations(ops);
store.contains_key(&key) && store[&key] == value
}
#[quickcheck]
fn prop_remove_then_not_contains(key: String, value: i32) -> bool {
let ops = vec![
StoreOperation::Insert(key.clone(), value),
StoreOperation::Remove(key.clone()),
];
let store = apply_operations(ops);
!store.contains_key(&key)
}
// Property testing for string operations
#[quickcheck]
fn prop_string_concatenation_length(s1: String, s2: String) -> bool {
let concatenated = format!("{}{}", s1, s2);
concatenated.len() == s1.len() + s2.len()
}
#[quickcheck]
fn prop_string_split_join_identity(s: String) -> TestResult {
if s.contains(',') {
return TestResult::discard();
}
let parts = vec![s.clone()];
let rejoined = parts.join(",");
TestResult::from_bool(rejoined == s)
}
// Property testing for mathematical operations
#[quickcheck]
fn prop_modular_arithmetic(a: i32, b: i32) -> TestResult {
if b == 0 {
return TestResult::discard();
}
let quotient = a / b;
let remainder = a % b;
TestResult::from_bool(a == b * quotient + remainder)
}
}
Mock Objects and Test Doubles
Manual Mocking
// Trait for dependency injection
pub trait EmailService {
fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<(), String>;
}
// Real implementation
pub struct RealEmailService;
impl EmailService for RealEmailService {
fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<(), String> {
// Actually send email
println!("Sending email to {}: {}", to, subject);
Ok(())
}
}
// Mock implementation for testing
#[cfg(test)]
pub struct MockEmailService {
pub emails_sent: std::cell::RefCell<Vec<(String, String, String)>>,
pub should_fail: bool,
}
#[cfg(test)]
impl MockEmailService {
pub fn new() -> Self {
MockEmailService {
emails_sent: std::cell::RefCell::new(Vec::new()),
should_fail: false,
}
}
pub fn set_should_fail(&mut self, should_fail: bool) {
self.should_fail = should_fail;
}
pub fn emails_sent(&self) -> std::cell::Ref<Vec<(String, String, String)>> {
self.emails_sent.borrow()
}
}
#[cfg(test)]
impl EmailService for MockEmailService {
fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<(), String> {
if self.should_fail {
return Err("Email service is down".to_string());
}
self.emails_sent.borrow_mut().push((
to.to_string(),
subject.to_string(),
body.to_string(),
));
Ok(())
}
}
// Service that uses email service
pub struct UserService<T: EmailService> {
email_service: T,
}
impl<T: EmailService> UserService<T> {
pub fn new(email_service: T) -> Self {
UserService { email_service }
}
pub fn register_user(&self, email: &str, name: &str) -> Result<(), String> {
// Validate email
if !email.contains('@') {
return Err("Invalid email".to_string());
}
// Send welcome email
let subject = "Welcome!";
let body = format!("Hello {}, welcome to our service!", name);
self.email_service.send_email(email, subject, &body)?;
Ok(())
}
}
#[cfg(test)]
mod mock_tests {
use super::*;
#[test]
fn test_user_registration_success() {
let mock_email = MockEmailService::new();
let user_service = UserService::new(mock_email);
let result = user_service.register_user("[email protected]", "Alice");
assert!(result.is_ok());
let emails = user_service.email_service.emails_sent();
assert_eq!(emails.len(), 1);
assert_eq!(emails[0].0, "[email protected]");
assert_eq!(emails[0].1, "Welcome!");
assert!(emails[0].2.contains("Alice"));
}
#[test]
fn test_user_registration_invalid_email() {
let mock_email = MockEmailService::new();
let user_service = UserService::new(mock_email);
let result = user_service.register_user("invalid-email", "Alice");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Invalid email");
let emails = user_service.email_service.emails_sent();
assert_eq!(emails.len(), 0);
}
#[test]
fn test_email_service_failure() {
let mut mock_email = MockEmailService::new();
mock_email.set_should_fail(true);
let user_service = UserService::new(mock_email);
let result = user_service.register_user("[email protected]", "Alice");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Email service is down");
}
}
Using Mockall Crate
Add to Cargo.toml
:
[dev-dependencies]
mockall = "0.11"
use mockall::{automock, predicate::*};
#[automock]
pub trait Database {
fn get_user(&self, id: u32) -> Option<User>;
fn save_user(&mut self, user: &User) -> Result<(), String>;
fn delete_user(&mut self, id: u32) -> Result<bool, String>;
}
#[derive(Debug, Clone, PartialEq)]
pub struct User {
pub id: u32,
pub name: String,
pub email: String,
}
pub struct UserRepository<D: Database> {
database: D,
}
impl<D: Database> UserRepository<D> {
pub fn new(database: D) -> Self {
UserRepository { database }
}
pub fn find_user(&self, id: u32) -> Option<User> {
self.database.get_user(id)
}
pub fn create_user(&mut self, name: String, email: String) -> Result<User, String> {
if name.is_empty() {
return Err("Name cannot be empty".to_string());
}
let user = User {
id: 123, // In real implementation, this would be generated
name,
email,
};
self.database.save_user(&user)?;
Ok(user)
}
pub fn remove_user(&mut self, id: u32) -> Result<bool, String> {
self.database.delete_user(id)
}
}
#[cfg(test)]
mod mockall_tests {
use super::*;
use mockall::predicate::eq;
#[test]
fn test_find_user_exists() {
let mut mock_db = MockDatabase::new();
let expected_user = User {
id: 1,
name: "Alice".to_string(),
email: "[email protected]".to_string(),
};
mock_db
.expect_get_user()
.with(eq(1))
.times(1)
.returning(move |_| Some(expected_user.clone()));
let repo = UserRepository::new(mock_db);
let user = repo.find_user(1);
assert!(user.is_some());
assert_eq!(user.unwrap().name, "Alice");
}
#[test]
fn test_find_user_not_exists() {
let mut mock_db = MockDatabase::new();
mock_db
.expect_get_user()
.with(eq(999))
.times(1)
.returning(|_| None);
let repo = UserRepository::new(mock_db);
let user = repo.find_user(999);
assert!(user.is_none());
}
#[test]
fn test_create_user_success() {
let mut mock_db = MockDatabase::new();
mock_db
.expect_save_user()
.times(1)
.returning(|_| Ok(()));
let mut repo = UserRepository::new(mock_db);
let result = repo.create_user("Bob".to_string(), "[email protected]".to_string());
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.name, "Bob");
assert_eq!(user.email, "[email protected]");
}
#[test]
fn test_create_user_empty_name() {
let mock_db = MockDatabase::new();
let mut repo = UserRepository::new(mock_db);
let result = repo.create_user("".to_string(), "[email protected]".to_string());
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Name cannot be empty");
}
#[test]
fn test_create_user_database_error() {
let mut mock_db = MockDatabase::new();
mock_db
.expect_save_user()
.times(1)
.returning(|_| Err("Database connection failed".to_string()));
let mut repo = UserRepository::new(mock_db);
let result = repo.create_user("Charlie".to_string(), "[email protected]".to_string());
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Database connection failed");
}
#[test]
fn test_remove_user_success() {
let mut mock_db = MockDatabase::new();
mock_db
.expect_delete_user()
.with(eq(1))
.times(1)
.returning(|_| Ok(true));
let mut repo = UserRepository::new(mock_db);
let result = repo.remove_user(1);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
}
Test-Driven Development (TDD)
TDD Cycle Example
// Step 1: Write failing test
#[cfg(test)]
mod tdd_example {
use super::*;
#[test]
fn test_calculator_add() {
let calc = Calculator::new();
assert_eq!(calc.add(2, 3), 5);
}
}
// Step 2: Write minimal code to make test pass
pub struct Calculator;
impl Calculator {
pub fn new() -> Self {
Calculator
}
pub fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
}
// Step 3: Write more tests
#[cfg(test)]
mod tdd_more_tests {
use super::*;
#[test]
fn test_calculator_subtract() {
let calc = Calculator::new();
assert_eq!(calc.subtract(5, 3), 2);
}
#[test]
fn test_calculator_multiply() {
let calc = Calculator::new();
assert_eq!(calc.multiply(4, 3), 12);
}
#[test]
fn test_calculator_divide() {
let calc = Calculator::new();
assert_eq!(calc.divide(10, 2), Ok(5));
}
#[test]
fn test_calculator_divide_by_zero() {
let calc = Calculator::new();
assert_eq!(calc.divide(10, 0), Err("Division by zero"));
}
#[test]
fn test_calculator_chain_operations() {
let calc = Calculator::new();
let result = calc.add(2, 3);
let result = calc.multiply(result, 2);
assert_eq!(result, 10);
}
}
// Step 4: Implement remaining functionality
impl Calculator {
pub fn subtract(&self, a: i32, b: i32) -> i32 {
a - b
}
pub fn multiply(&self, a: i32, b: i32) -> i32 {
a * b
}
pub fn divide(&self, a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("Division by zero")
} else {
Ok(a / b)
}
}
}
TDD for Complex Features
// Example: Building a shopping cart with TDD
#[derive(Debug, Clone, PartialEq)]
pub struct Item {
pub id: String,
pub name: String,
pub price: f64,
}
#[derive(Debug, PartialEq)]
pub enum CartError {
ItemNotFound,
InvalidQuantity,
PriceMismatch,
}
pub struct ShoppingCart {
items: std::collections::HashMap<String, (Item, u32)>,
}
#[cfg(test)]
mod shopping_cart_tdd {
use super::*;
#[test]
fn test_new_cart_is_empty() {
let cart = ShoppingCart::new();
assert_eq!(cart.item_count(), 0);
assert_eq!(cart.total_price(), 0.0);
}
#[test]
fn test_add_item_to_cart() {
let mut cart = ShoppingCart::new();
let item = Item {
id: "1".to_string(),
name: "Apple".to_string(),
price: 1.0,
};
let result = cart.add_item(item.clone(), 3);
assert!(result.is_ok());
assert_eq!(cart.item_count(), 1);
assert_eq!(cart.quantity_of(&item.id), Some(3));
}
#[test]
fn test_add_same_item_twice() {
let mut cart = ShoppingCart::new();
let item = Item {
id: "1".to_string(),
name: "Apple".to_string(),
price: 1.0,
};
cart.add_item(item.clone(), 2).unwrap();
cart.add_item(item.clone(), 3).unwrap();
assert_eq!(cart.item_count(), 1);
assert_eq!(cart.quantity_of(&item.id), Some(5));
}
#[test]
fn test_remove_item_completely() {
let mut cart = ShoppingCart::new();
let item = Item {
id: "1".to_string(),
name: "Apple".to_string(),
price: 1.0,
};
cart.add_item(item.clone(), 3).unwrap();
let result = cart.remove_item(&item.id);
assert!(result.is_ok());
assert_eq!(cart.item_count(), 0);
assert_eq!(cart.quantity_of(&item.id), None);
}
#[test]
fn test_remove_nonexistent_item() {
let mut cart = ShoppingCart::new();
let result = cart.remove_item("nonexistent");
assert_eq!(result, Err(CartError::ItemNotFound));
}
#[test]
fn test_update_quantity() {
let mut cart = ShoppingCart::new();
let item = Item {
id: "1".to_string(),
name: "Apple".to_string(),
price: 1.0,
};
cart.add_item(item.clone(), 5).unwrap();
let result = cart.update_quantity(&item.id, 3);
assert!(result.is_ok());
assert_eq!(cart.quantity_of(&item.id), Some(3));
}
#[test]
fn test_total_price_calculation() {
let mut cart = ShoppingCart::new();
let apple = Item {
id: "1".to_string(),
name: "Apple".to_string(),
price: 1.0,
};
let banana = Item {
id: "2".to_string(),
name: "Banana".to_string(),
price: 0.5,
};
cart.add_item(apple, 3).unwrap(); // 3 * 1.0 = 3.0
cart.add_item(banana, 4).unwrap(); // 4 * 0.5 = 2.0
assert_eq!(cart.total_price(), 5.0);
}
}
// Implementation driven by tests
impl ShoppingCart {
pub fn new() -> Self {
ShoppingCart {
items: std::collections::HashMap::new(),
}
}
pub fn item_count(&self) -> usize {
self.items.len()
}
pub fn total_price(&self) -> f64 {
self.items
.values()
.map(|(item, quantity)| item.price * (*quantity as f64))
.sum()
}
pub fn add_item(&mut self, item: Item, quantity: u32) -> Result<(), CartError> {
if quantity == 0 {
return Err(CartError::InvalidQuantity);
}
self.items
.entry(item.id.clone())
.and_modify(|(_, q)| *q += quantity)
.or_insert((item, quantity));
Ok(())
}
pub fn remove_item(&mut self, item_id: &str) -> Result<(), CartError> {
match self.items.remove(item_id) {
Some(_) => Ok(()),
None => Err(CartError::ItemNotFound),
}
}
pub fn update_quantity(&mut self, item_id: &str, quantity: u32) -> Result<(), CartError> {
if quantity == 0 {
return self.remove_item(item_id);
}
match self.items.get_mut(item_id) {
Some((_, q)) => {
*q = quantity;
Ok(())
}
None => Err(CartError::ItemNotFound),
}
}
pub fn quantity_of(&self, item_id: &str) -> Option<u32> {
self.items.get(item_id).map(|(_, quantity)| *quantity)
}
}
Best Practices for Unit Testing
Test Organization and Naming
#[cfg(test)]
mod user_service_tests {
use super::*;
mod when_creating_user {
use super::*;
#[test]
fn should_succeed_with_valid_data() {
// Arrange
let service = UserService::new();
let user_data = CreateUserRequest {
name: "Alice".to_string(),
email: "[email protected]".to_string(),
};
// Act
let result = service.create_user(user_data);
// Assert
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.name, "Alice");
assert_eq!(user.email, "[email protected]");
}
#[test]
fn should_fail_with_empty_name() {
let service = UserService::new();
let user_data = CreateUserRequest {
name: "".to_string(),
email: "[email protected]".to_string(),
};
let result = service.create_user(user_data);
assert!(result.is_err());
}
#[test]
fn should_fail_with_invalid_email() {
let service = UserService::new();
let user_data = CreateUserRequest {
name: "Alice".to_string(),
email: "invalid-email".to_string(),
};
let result = service.create_user(user_data);
assert!(result.is_err());
}
}
mod when_updating_user {
use super::*;
#[test]
fn should_update_existing_user() {
// Test implementation
}
#[test]
fn should_fail_for_nonexistent_user() {
// Test implementation
}
}
}
// Helper struct for test setup
#[cfg(test)]
struct TestUserService;
#[cfg(test)]
impl TestUserService {
fn with_users(users: Vec<User>) -> UserService {
// Setup service with test data
todo!()
}
fn empty() -> UserService {
UserService::new()
}
}
Performance Testing in Unit Tests
use std::time::{Duration, Instant};
#[cfg(test)]
mod performance_tests {
use super::*;
#[test]
fn test_sort_performance() {
let mut data: Vec<i32> = (0..10000).rev().collect(); // Worst case for some algorithms
let start = Instant::now();
data.sort();
let duration = start.elapsed();
println!("Sorting 10,000 elements took: {:?}", duration);
assert!(duration < Duration::from_millis(100), "Sorting took too long: {:?}", duration);
// Verify it's actually sorted
assert!(data.windows(2).all(|w| w[0] <= w[1]));
}
#[test]
fn test_hash_map_performance() {
use std::collections::HashMap;
let mut map = HashMap::new();
let start = Instant::now();
for i in 0..10000 {
map.insert(i, i * 2);
}
let insert_duration = start.elapsed();
let start = Instant::now();
for i in 0..10000 {
assert_eq!(map.get(&i), Some(&(i * 2)));
}
let lookup_duration = start.elapsed();
println!("HashMap insert: {:?}, lookup: {:?}", insert_duration, lookup_duration);
assert!(insert_duration < Duration::from_millis(50));
assert!(lookup_duration < Duration::from_millis(10));
}
}
Unit testing in Rust provides powerful tools for ensuring code correctness. The built-in test framework, combined with property-based testing and mocking libraries, enables comprehensive testing strategies. Always aim for clear, maintainable tests that document your code's behavior and catch regressions early.