Test modules, assertions, and organization
Unit testing verifies individual components work correctly in isolation. Rust has first-class testing support built into the language and Cargo, making it easy to write and run tests alongside your code.
Effective unit testing in Rust requires understanding:
/// A simple calculator module
pub mod calculator {
/// Add two numbers
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Subtract b from a
pub fn subtract(a: i32, b: i32) -> i32 {
a - b
}
/// Multiply two numbers
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
/// Divide a by b, returning None if b is zero
pub fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
/// Private helper function
fn validate_positive(n: i32) -> bool {
n > 0
}
/// Calculate factorial
pub fn factorial(n: u32) -> u64 {
if n <= 1 {
1
} else {
n as u64 * factorial(n - 1)
}
}
// Unit tests in the same module
#[cfg(test)]
mod tests {
use super::*;
// Basic test
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
// Multiple assertions
#[test]
fn test_add_various_cases() {
assert_eq!(add(0, 0), 0);
assert_eq!(add(-1, 1), 0);
assert_eq!(add(-5, -3), -8);
assert_eq!(add(i32::MAX, 0), i32::MAX);
}
// Test with message
#[test]
fn test_subtract() {
let result = subtract(10, 4);
assert_eq!(result, 6, "Expected 10 - 4 = 6, got {}", result);
}
// Test that should panic
#[test]
#[should_panic]
fn test_overflow_panics() {
let _ = add(i32::MAX, 1); // This will panic in debug mode
}
// Test with expected panic message
#[test]
#[should_panic(expected = "attempt to add with overflow")]
fn test_overflow_panic_message() {
let _ = add(i32::MAX, 1);
}
// Test Option return values
#[test]
fn test_divide_success() {
assert_eq!(divide(10, 2), Some(5));
}
#[test]
fn test_divide_by_zero() {
assert_eq!(divide(10, 0), None);
}
// Using assert! for boolean conditions
#[test]
fn test_multiply_positive() {
let result = multiply(3, 4);
assert!(result > 0, "Product of positive numbers should be positive");
assert!(result == 12);
}
// Testing private functions (allowed in same module)
#[test]
fn test_validate_positive() {
assert!(validate_positive(5));
assert!(!validate_positive(0));
assert!(!validate_positive(-3));
}
// Ignored test (run with `cargo test -- --ignored`)
#[test]
#[ignore]
fn test_expensive_computation() {
let result = factorial(20);
assert_eq!(result, 2_432_902_008_176_640_000);
}
}
}
/// Testing structs and methods
pub struct BankAccount {
balance: i64,
owner: String,
}
impl BankAccount {
pub fn new(owner: &str, initial_balance: i64) -> Self {
BankAccount {
balance: initial_balance,
owner: owner.to_string(),
}
}
pub fn deposit(&mut self, amount: i64) -> Result<(), &'static str> {
if amount <= 0 {
return Err("Deposit amount must be positive");
}
self.balance += amount;
Ok(())
}
pub fn withdraw(&mut self, amount: i64) -> Result<(), &'static str> {
if amount <= 0 {
return Err("Withdrawal amount must be positive");
}
if amount > self.balance {
return Err("Insufficient funds");
}
self.balance -= amount;
Ok(())
}
pub fn balance(&self) -> i64 {
self.balance
}
pub fn owner(&self) -> &str {
&self.owner
}
}
#[cfg(test)]
mod bank_tests {
use super::*;
// Setup helper function
fn create_account() -> BankAccount {
BankAccount::new("Test User", 1000)
}
#[test]
fn test_new_account() {
let account = BankAccount::new("Alice", 500);
assert_eq!(account.owner(), "Alice");
assert_eq!(account.balance(), 500);
}
#[test]
fn test_deposit() {
let mut account = create_account();
assert!(account.deposit(100).is_ok());
assert_eq!(account.balance(), 1100);
}
#[test]
fn test_deposit_negative() {
let mut account = create_account();
let result = account.deposit(-50);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Deposit amount must be positive");
}
#[test]
fn test_withdraw_success() {
let mut account = create_account();
assert!(account.withdraw(500).is_ok());
assert_eq!(account.balance(), 500);
}
#[test]
fn test_withdraw_insufficient_funds() {
let mut account = create_account();
let result = account.withdraw(2000);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Insufficient funds");
assert_eq!(account.balance(), 1000); // Balance unchanged
}
// Test using matches! macro
#[test]
fn test_result_matching() {
let mut account = create_account();
let result = account.withdraw(100);
assert!(matches!(result, Ok(())));
let result = account.withdraw(10000);
assert!(matches!(result, Err(_)));
}
}
/// Testing with fixtures using a test module
pub mod user_service {
use std::collections::HashMap;
pub struct User {
pub id: u64,
pub name: String,
pub email: String,
}
pub struct UserService {
users: HashMap<u64, User>,
next_id: u64,
}
impl UserService {
pub fn new() -> Self {
UserService {
users: HashMap::new(),
next_id: 1,
}
}
pub fn create_user(&mut self, name: &str, email: &str) -> u64 {
let id = self.next_id;
self.next_id += 1;
self.users.insert(id, User {
id,
name: name.to_string(),
email: email.to_string(),
});
id
}
pub fn get_user(&self, id: u64) -> Option<&User> {
self.users.get(&id)
}
pub fn delete_user(&mut self, id: u64) -> bool {
self.users.remove(&id).is_some()
}
pub fn user_count(&self) -> usize {
self.users.len()
}
}
impl Default for UserService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
// Fixture struct for test setup
struct TestFixture {
service: UserService,
alice_id: u64,
bob_id: u64,
}
impl TestFixture {
fn new() -> Self {
let mut service = UserService::new();
let alice_id = service.create_user("Alice", "alice@example.com");
let bob_id = service.create_user("Bob", "bob@example.com");
TestFixture { service, alice_id, bob_id }
}
}
#[test]
fn test_create_user() {
let mut service = UserService::new();
let id = service.create_user("Test", "test@example.com");
assert_eq!(id, 1);
assert_eq!(service.user_count(), 1);
}
#[test]
fn test_get_user_with_fixture() {
let fixture = TestFixture::new();
let alice = fixture.service.get_user(fixture.alice_id).unwrap();
assert_eq!(alice.name, "Alice");
assert_eq!(alice.email, "alice@example.com");
}
#[test]
fn test_delete_user() {
let mut fixture = TestFixture::new();
assert!(fixture.service.delete_user(fixture.alice_id));
assert_eq!(fixture.service.user_count(), 1);
assert!(fixture.service.get_user(fixture.alice_id).is_none());
}
#[test]
fn test_delete_nonexistent_user() {
let mut fixture = TestFixture::new();
assert!(!fixture.service.delete_user(999));
assert_eq!(fixture.service.user_count(), 2);
}
}
}
fn main() {
println!("Run tests with: cargo test");
println!("Run specific test: cargo test test_add");
println!("Run ignored tests: cargo test -- --ignored");
println!("Show output: cargo test -- --nocapture");
}
#[cfg(test)]: Test code only compiled when testing#[test]: Marks function as a test caseassert! macros: Provide clear failure messagessrc/
āāā lib.rs
ā āāā #[cfg(test)] mod tests { } # Unit tests
āāā calculator.rs
ā āāā #[cfg(test)] mod tests { } # Module tests
tests/
āāā integration_test.rs # Integration tests
āāā common/
āāā mod.rs # Shared test utilities
| Macro | Purpose | Example |
|-------|---------|---------|
| assert! | Boolean condition | assert!(x > 0) |
| assert_eq! | Equality | assert_eq!(a, b) |
| assert_ne! | Inequality | assert_ne!(a, b) |
| debug_assert! | Debug-only | debug_assert!(valid) |
// DON'T: Test multiple unrelated things
#[test]
fn test_everything() {
assert_eq!(add(1, 2), 3);
assert_eq!(multiply(2, 3), 6);
assert!(divide(10, 2).is_some());
}
// DO: One concept per test
#[test]
fn test_add_positive_numbers() {
assert_eq!(add(1, 2), 3);
}
// DON'T: Assertions without context
#[test]
fn test_calc() {
assert!(result > 0); // What is result? Why > 0?
}
// DO: Clear assertions with messages
#[test]
fn test_deposit_increases_balance() {
let result = account.balance();
assert!(result > initial, "Deposit should increase balance");
}
factorial (0, 1, large numbers)Run this code in the official Rust Playground