Intermediate Level
Testing
Chapter 14: Testing ๐งช
Write tests to ensure your Rust code works correctly and catch bugs early.
Writing Tests
Basic Test Structure
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}Running Tests
# Run all tests
cargo test
# Run tests in a specific file
cargo test --lib
# Run a specific test
cargo test it_works
# Run tests in a specific module
cargo test tests
# Run tests with output shown
cargo test -- --nocaptureTest Macros
assert_eq! and assert_ne!
#[cfg(test)]
mod tests {
#[test]
fn equality_tests() {
assert_eq!(2 + 2, 4);
assert_ne!(3 + 3, 7);
}
#[test]
fn string_tests() {
let greeting = "Hello";
let target = "Hello";
assert_eq!(greeting, target);
}
}assert!
#[cfg(test)]
mod tests {
#[test]
fn greater_than_zero() {
let value = 5;
assert!(value > 0);
}
#[test]
fn contains_value() {
let list = vec![1, 2, 3, 4, 5];
assert!(list.contains(&3));
}
}Testing Functions
Example Function to Test
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
assert_eq!(4, add_two(2));
}
#[test]
fn add_three_and_two() {
assert_eq!(5, add_two(3));
}
}Test Organization
Unit Tests
// src/lib.rs
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(String::from("Cannot divide by zero"))
} else {
Ok(a / b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
assert_eq!(add(-1, 1), 0);
}
#[test]
fn test_divide() {
assert_eq!(divide(10.0, 2.0), Ok(5.0));
assert_eq!(divide(10.0, 0.0), Err(String::from("Cannot divide by zero")));
}
}Integration Tests
Create tests/integration_test.rs:
use my_crate;
#[test]
fn it_adds() {
assert_eq!(my_crate::add(2, 3), 5);
}
#[test]
fn it_divides() {
assert_eq!(my_crate::divide(10.0, 2.0), Ok(5.0));
}Test Attributes
#[should_panic]
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
#[test]
#[should_panic(expected = "Guess value must be between 1 and 100")]
fn greater_than_100_detailed() {
Guess::new(200);
}
}#[ignore]
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// This test takes a long time to run
assert_eq!(1000000 + 1, 1000001);
}
}
// Run ignored tests
cargo test -- --ignoredCustom Failures
Adding Custom Messages
#[cfg(test)]
mod tests {
#[test]
fn greeting_contains_name() {
let greeting = "Hello, Rust!";
assert!(
greeting.contains("Rust"),
"Greeting did not contain name, value was `{}`",
greeting
);
}
}Using Result<T, E> in Tests
Returning Results from Tests
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}Test Setup and Teardown
Using setUp and tearDown Patterns
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}Practical Examples
Example 1: Testing a Math Library
Create src/math.rs:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn subtract(a: i32, b: i32) -> i32 {
a - b
}
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
pub fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Cannot divide by zero"))
} else {
Ok(a / b)
}
}
pub fn is_even(n: i32) -> bool {
n % 2 == 0
}
#[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_subtract() {
assert_eq!(subtract(5, 3), 2);
assert_eq!(subtract(0, 5), -5);
}
#[test]
fn test_multiply() {
assert_eq!(multiply(3, 4), 12);
assert_eq!(multiply(-2, 3), -6);
assert_eq!(multiply(0, 100), 0);
}
#[test]
fn test_divide() {
assert_eq!(divide(10.0, 2.0), Ok(5.0));
assert_eq!(divide(7.0, 2.0), Ok(3.5));
assert!(divide(10.0, 0.0).is_err());
}
#[test]
fn test_is_even() {
assert!(is_even(4));
assert!(!is_even(5));
assert!(is_even(0));
assert!(!is_even(-3));
assert!(is_even(-4));
}
}Example 2: Testing a User Authentication Module
Create src/auth.rs:
use std::collections::HashMap;
pub struct User {
pub username: String,
pub email: String,
pub password_hash: String,
}
pub struct AuthSystem {
users: HashMap<String, User>,
}
impl AuthSystem {
pub fn new() -> AuthSystem {
AuthSystem {
users: HashMap::new(),
}
}
pub fn register(&mut self, username: String, email: String, password: String) -> Result<(), String> {
if self.users.contains_key(&username) {
return Err(String::from("Username already exists"));
}
if password.len() < 8 {
return Err(String::from("Password must be at least 8 characters"));
}
let user = User {
username: username.clone(),
email,
password_hash: hash_password(&password),
};
self.users.insert(username, user);
Ok(())
}
pub fn login(&self, username: &str, password: &str) -> Result<&User, String> {
match self.users.get(username) {
Some(user) => {
if verify_password(&user.password_hash, password) {
Ok(user)
} else {
Err(String::from("Invalid password"))
}
}
None => Err(String::from("User not found")),
}
}
}
fn hash_password(password: &str) -> String {
// Simple mock hashing function
format!("hashed_{}", password)
}
fn verify_password(hash: &str, password: &str) -> bool {
hash == &hash_password(password)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_register_success() {
let mut auth = AuthSystem::new();
let result = auth.register(
String::from("alice"),
String::from("alice@example.com"),
String::from("password123")
);
assert!(result.is_ok());
assert!(auth.users.contains_key("alice"));
}
#[test]
fn test_register_duplicate_username() {
let mut auth = AuthSystem::new();
auth.register(
String::from("alice"),
String::from("alice@example.com"),
String::from("password123")
).unwrap();
let result = auth.register(
String::from("alice"),
String::from("alice2@example.com"),
String::from("password456")
);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Username already exists");
}
#[test]
fn test_register_short_password() {
let mut auth = AuthSystem::new();
let result = auth.register(
String::from("bob"),
String::from("bob@example.com"),
String::from("123")
);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Password must be at least 8 characters");
}
#[test]
fn test_login_success() {
let mut auth = AuthSystem::new();
auth.register(
String::from("alice"),
String::from("alice@example.com"),
String::from("password123")
).unwrap();
let result = auth.login("alice", "password123");
assert!(result.is_ok());
}
#[test]
fn test_login_invalid_password() {
let mut auth = AuthSystem::new();
auth.register(
String::from("alice"),
String::from("alice@example.com"),
String::from("password123")
).unwrap();
let result = auth.login("alice", "wrongpassword");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Invalid password");
}
#[test]
fn test_login_user_not_found() {
let auth = AuthSystem::new();
let result = auth.login("charlie", "password123");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "User not found");
}
}Test Coverage
Measuring Test Coverage
# Install cargo-tarpaulin
cargo install cargo-tarpaulin
# Run coverage report
cargo tarpaulin --verboseCommon Mistakes
โ Testing Private Functions Directly
// This won't work because private functions aren't accessible in tests
#[cfg(test)]
mod tests {
#[test]
fn test_private_function() {
// private_function(); // Error: function is private
}
}โ Testing Through Public Interface
// Test private functionality through public functions
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_functionality() {
// Test the public interface that uses private functions internally
let result = public_function();
assert_eq!(result, expected_value);
}
}โ Not Handling Test Failures Properly
#[test]
fn bad_test() {
let result = divide(10.0, 0.0);
// This test will panic because result is Err and we're not handling it
assert_eq!(result, Ok(5.0));
}โ Proper Error Handling in Tests
#[test]
fn good_test() {
let result = divide(10.0, 0.0);
assert!(result.is_err());
// Or
assert_eq!(result, Err(String::from("Cannot divide by zero")));
}Key Takeaways
- โ
Use
#[cfg(test)]to mark test modules - โ
Use
assert!,assert_eq!, andassert_ne!for assertions - โ Add custom failure messages for better debugging
- โ
Use
#[should_panic]for tests that should panic - โ
Use
#[ignore]for expensive or incomplete tests - โ Organize tests into unit tests (in source files) and integration tests (in tests directory)
- โ Test both success and failure cases
- โ
Follow Rust naming conventions (test function names start with
test_)
Ready for Chapter 15? โ Iterators