Testing in Rust: Ensuring Code Quality

About This Module

This short module introduces testing in Rust, covering how to write effective unit tests, integration tests, and use Rust's built-in testing framework. You'll learn testing best practices and understand why comprehensive testing is crucial for reliable software development.

Prework

Prework Reading

Please read the following sections from The Rust Programming Language Book:

  • Chapter 11: Writing Automated Tests
  • Chapter 11.1: How to Write Tests
  • Chapter 11.2: Controlling How Tests Are Run
  • Chapter 11.3: Test Organization

Pre-lecture Reflections

  1. Why is testing important in software development, especially in systems programming?
  2. How does Rust's testing framework compare to testing frameworks you've used in other languages?
  3. What is the difference between unit tests, integration tests, and documentation tests?
  4. What makes a good test case?

Learning Objectives

By the end of this module, you will be able to:

  • Write unit tests using Rust's testing framework
  • Use assertions effectively in tests
  • Organize and run test suites
  • Understand testing best practices and test-driven development

Tests

  • Why are tests useful?
  • What is typical test to functional code ratio?

730K lines of code in Meta proxy server, roughly 1:1 ratio of tests to actual code. https://github.com/facebook/proxygen

Creating a Library Crate

You can use cargo to create a library project:

$ cargo new adder --lib
     Created library `adder` project
$ cd adder

This will create a new project in the adder directory with the following structure:

.
├── Cargo.lock
├── Cargo.toml
└── src
    └── lib.rs

Library Crate Code

Similar to the "Hello, world!" binary crate, the library crate is prepopulated with some minimal code.

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}
  • The #[cfg(test)] attribute tells Rust to compile and run the tests only when you run cargo test.

  • The use super::*; line tells Rust to bring all the items defined in the outer scope into the scope of the tests module.

  • The #[test] attribute tells Rust that the function is a test function.

  • The assert_eq!(result, 4); line tells Rust to check that the result of the add function is equal to 4.

    • assert! is a macro that takes a boolean expression and panics if the expression is false.
    • there are many other assert! macros, including assert_ne!, assert_approx_eq!, etc.

Running the Tests

You can run the tests with the cargo test command.

% cargo test
   Compiling adder v0.1.0 (...path_to_adder/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.50s
     Running unittests src/lib.rs (target/debug/deps/adder-1dfa21403f25b3c4)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  • 0 ignored means no tests were ignored with the #[ignore] attribute.
  • 0 measured means no tests were measured with Rust's built-in benchmarking framework.
  • 0 filtered out means no subset of tests were specified
  • Doc-tests automatically test any example code that is provided in /// comments.

Example Unit Test Code

Here is an example of a set of tests for a function that doubles the elements of a vector.

fn doubleme(inp: &Vec<f64>) -> Vec<f64> {
    let mut nv = inp.clone();
    for (i, x) in inp.iter().enumerate() {
        nv[i] = *x * 2.0;
    }
    nv
}

#[test]
fn test_doubleme_positive() {
    let v = vec![1.0, 2.0, 3.0];
    let w = doubleme(&v);
    for (x, y) in v.iter().zip(w.iter()) {
        assert_eq!(*y, 2.0 * *x, "Element is not double");
    }
}
#[test]
fn test_doubleme_negative() {
    let v = vec![-1.0, -2.0, -3.0];
    let w = doubleme(&v);
    for (x, y) in v.iter().zip(w.iter()) {
        assert_eq!(*y, 2.0 * *x, "Negative element is not double");
    }
}
#[test]
fn test_doubleme_zero() {
    let v = vec![0.0];
    let w = doubleme(&v);
    for (x, y) in v.iter().zip(w.iter()) {
        assert_eq!(*y, 2.0 * *x, "Zero element is not double");
    }
}
#[test]
fn test_doubleme_empty() {
    let v: Vec<f64> = vec![];
    let w = doubleme(&v);
    assert_eq!(w.len(), 0, "Empty Vector is not empty");
}

fn testme() {
    let v: Vec<f64> = vec![2.0, 3.0, 4.0];
    let w = doubleme(&v);
    println!("V = {:?} W = {:?}", v, w);
}

fn main() {
    testme();
}

Further Reading

Read 11.1 How to Write Tests for more information.

In-Class Activity

In this activity, you will write tests for a function that finds the second largest element in a slice of integers.

Be creative with your tests! With the right tests, you will be able to find the bug in the function.

Fix the bug in the function so all tests pass.

Part 1: Create a New Library Project

Create a new Rust library project:

cargo new --lib testing_practice
cd testing_practice

Part 2: Implement and Test

Replace the contents of src/lib.rs with the following function:

/// Returns the second largest element in a slice of integers.
/// Returns None if there are fewer than 2 distinct elements.
///
/// # Examples
/// ```
/// use testing_practice::second_largest;
/// assert_eq!(second_largest(&[1, 2, 3]), Some(2));
/// assert_eq!(second_largest(&[5, 5, 5]), None);
/// ```
pub fn second_largest(numbers: &[i32]) -> Option<i32> {
    if numbers.len() < 2 {
        return None;
    }
    
    let mut largest = numbers[0];
    let mut second = numbers[1];
    
    if second > largest {
        std::mem::swap(&mut largest, &mut second);
    }
    
    for &num in &numbers[2..] {
        if num > largest {
            second = largest;
            largest = num;
        } else if num > second {
            second = num;
        }
    }
    
    if largest == second {
        None
    } else {
        Some(second)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_all_same() {
        let result = second_largest(&[1, 1, 1]);
        assert_eq!(result, None);
    }
}

Part 3: Write Tests

Your task is to write at least 3-4 comprehensive tests for this function. Think about:

  • Normal cases
  • Edge cases (empty, single element, etc.)
  • Special cases (all same values, duplicates of largest, etc.)

Add your tests in a #[cfg(test)] module below the function.

Part 4: Debug

Run cargo test. If any of your tests fail, there is a bug in the function. Your goal is to:

  1. Identify what test case reveals the bug
  2. Understand why the function fails
  3. Fix the function so all tests pass

Hint: Think carefully about what happens when the largest element appears multiple times in the array.

Part 5: Submit

Submit your code to Gradescope.