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
- Why is testing important in software development, especially in systems programming?
- How does Rust's testing framework compare to testing frameworks you've used in other languages?
- What is the difference between unit tests, integration tests, and documentation tests?
- 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 runcargo 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 theaddfunction 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 ignoredmeans no tests were ignored with the#[ignore]attribute.0 measuredmeans no tests were measured with Rust's built-in benchmarking framework.0 filtered outmeans 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:
- Identify what test case reveals the bug
- Understand why the function fails
- 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.