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 -- --nocapture

Test 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 -- --ignored

Custom 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 --verbose

Common 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!, and assert_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

๐Ÿฆ€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS โ€ข Open Source