Activity 28: Write Tests for Your Code

Goal

Practice writing unit tests for Rust code using the #[test] attribute and assert macros.

Setup Steps

1. Create a new Cargo project:

cargo new contact_tests
cd contact_tests

2. Create the module files:

Your project should have this structure:

contact_tests/
├── Cargo.toml
└── src/
    ├── main.rs      (already exists - replace with code below)
    ├── person.rs    (create this file)
    ├── storage.rs   (create this file)
    └── stats.rs     (create this file)

3. Copy the code below into each file

Use the base code provided in the next section.

4. Verify it compiles:

cargo run

You should see output with counts, names, averages, etc.

Your Task

Add test modules to person.rs, storage.rs, and stats.rs (NOT main.rs).

What to test:

  • Normal cases that should work
  • Edge cases (empty inputs, boundary values, single items)
  • Invalid inputs (use #[should_panic] for functions that panic)
  • Custom error messages to explain test failures

How to run tests:

cargo test

Challenge yourself:

  1. Write 3-4 tests per module
  2. Add custom error messages to at least 2 tests
  3. Try breaking the code to see tests fail (then fix it!)
  4. Choose two tests you're proud of to submit on Gradescope with explanations

Base code

person.rs:

#![allow(unused)]
fn main() {
pub struct Person {      // [H][a] - pub - used by main
    name: String,        // private - only accessed through methods
    age: i32,            // private
    score: f64,          // private
}

fn validate_age(age: i32) -> bool {     
    age > 0 && age < 150
}

impl Person {
    pub fn new(name: String, age: i32, score: f64) -> Person {  
        if validate_age(age) {
            Person { name, age, score }
        } else {
            panic!("Invalid age");
        }
    }

    pub fn get_age(&self) -> i32 {      
        self.age
    }

    pub fn get_name(&self) -> &str {    
        &self.name
    }

    pub fn get_score(&self) -> f64 {    
        self.score
    }
}
}

storage.rs

#![allow(unused)]
fn main() {
use crate::person::Person; // needs Person struct!

pub fn add_person(people: &mut Vec<Person>, person: Person) {  
    people.push(person);
}

pub fn count_people(people: &Vec<Person>) -> usize {  
    people.len()
}

pub fn list_names(people: &Vec<Person>) -> Vec<String> {  
    people.iter()
        .map(|p| p.get_name().to_string())
        .collect()
}

pub fn format_person(p: &Person) -> String {  
    format!("{} (age {}, score: {:.1})",
            p.get_name(),
            p.get_age(),
            p.get_score())
}
}

stats.rs

#![allow(unused)]
fn main() {
use crate::person::Person; // needs Person struct!

const MIN_PASSING_SCORE: f64 = 60.0;  

pub fn average_score(people: &Vec<Person>) -> f64 {  
    let sum: f64 = people.iter()
        .map(|p| p.get_score())
        .sum();
    compute_average(sum, people.len())
}

fn compute_average(sum: f64, count: usize) -> f64 {  
    sum / count as f64
}

pub fn highest_score(people: &Vec<Person>) -> f64 {  
    people.iter()
        .map(|p| p.get_score())
        .max_by(|a, b| a.partial_cmp(b).unwrap())
        .unwrap()
}

pub fn count_passing(people: &Vec<Person>) -> usize {  
    people.iter()
        .filter(|p| p.get_score() >= MIN_PASSING_SCORE)
        .count()
}
}

main.rs

mod person;
mod storage;
mod stats;

use person::Person;
use storage::{add_person, count_people, list_names};
use stats::{average_score, highest_score, count_passing};

fn main() {
    let mut people = Vec::new();

    let alice = Person::new("Alice".to_string(), 25, 92.5);
    let bob = Person::new("Bob".to_string(), 30, 87.0);

    add_person(&mut people, alice);
    add_person(&mut people, bob);

    println!("Count: {}", count_people(&people));
    println!("Names: {:?}", list_names(&people));
    println!("Average: {:.1}", average_score(&people));
    println!("Highest: {:.1}", highest_score(&people));
    println!("Passing: {}", count_passing(&people));
}