#19 Testing
#[test], assert, cargo test
Your first test
In Rust, tests are regular functions annotated with #[test]. A test passes if it does not panic. You typically place tests in a tests module at the bottom of the file:
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
let result = add(2, 3);
assert_eq!(result, 5);
}
}The #[cfg(test)] attribute tells the compiler to only compile this module when running tests. The use super::* imports everything from the parent module so you can test private functions.
Assert macros
Rust provides several macros for making assertions in tests:
#[cfg(test)]
mod tests {
#[test]
fn test_assert() {
// assert! checks a boolean condition
assert!(1 + 1 == 2);
assert!(true);
}
#[test]
fn test_assert_eq() {
// assert_eq! checks that two values are equal
assert_eq!(2 + 2, 4);
assert_eq!("hello".to_uppercase(), "HELLO");
}
#[test]
fn test_assert_ne() {
// assert_ne! checks that two values are NOT equal
assert_ne!(1, 2);
assert_ne!("hello", "world");
}
#[test]
fn test_with_message() {
// Add a custom message on failure
let result = 2 + 2;
assert_eq!(result, 4, "Expected 4, but got {}", result);
}
}You can also test that code panics using #[should_panic]:
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero!");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Division by zero")]
fn test_divide_by_zero() {
divide(10, 0);
}
}Testing with Result
Tests can also return Result instead of panicking. This lets you use the ? operator inside tests:
fn parse_port(s: &str) -> Result<u16, String> {
s.parse::<u16>().map_err(|e| format!("Invalid port: {}", e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_port() -> Result<(), String> {
let port = parse_port("8080")?;
assert_eq!(port, 8080);
Ok(())
}
#[test]
fn test_invalid_port() {
let result = parse_port("not_a_number");
assert!(result.is_err());
}
#[test]
fn test_port_range() -> Result<(), String> {
let port = parse_port("443")?;
assert!(port > 0, "Port must be positive");
Ok(())
}
}Integration tests
Integration tests live in a tests/ directory next to src/. They test your library as an external user would:
my-project/
src/
lib.rs // Library code
main.rs // Binary (optional)
tests/
integration_test.rs // Integration testpub fn greeting(name: &str) -> String {
format!("Hello, {}!", name)
}
pub fn is_even(n: i32) -> bool {
n % 2 == 0
}// No #[cfg(test)] needed -- the tests/ directory is only compiled for testing
use testing_demo::{greeting, is_even};
#[test]
fn test_greeting() {
assert_eq!(greeting("Rust"), "Hello, Rust!");
}
#[test]
fn test_greeting_empty() {
assert_eq!(greeting(""), "Hello, !");
}
#[test]
fn test_even_numbers() {
assert!(is_even(2));
assert!(is_even(0));
assert!(is_even(-4));
}
#[test]
fn test_odd_numbers() {
assert!(!is_even(1));
assert!(!is_even(3));
assert!(!is_even(-7));
}Running tests
Cargo provides several ways to run tests:
# Run all tests
cargo test
# Run tests with output (println! visible)
cargo test -- --nocapture
# Run a specific test by name
cargo test test_add
# Run tests matching a pattern
cargo test greeting
# Run only integration tests
cargo test --test integration_test
# Run tests in a specific module
cargo test tests::test_add
# Run ignored tests
cargo test -- --ignoredYou can mark tests as ignored with #[ignore]. This is useful for slow tests or tests that require external resources:
#[test]
#[ignore]
fn expensive_test() {
// This test takes a long time
// Only runs with: cargo test -- --ignored
std::thread::sleep(std::time::Duration::from_secs(10));
assert!(true);
}Summary
#[cfg(test)] // Only compile for tests
mod tests {
use super::*; // Import parent module
#[test] // Mark as test
fn test_name() { }
#[test]
#[should_panic] // Expect a panic
fn test_panic() { }
#[test]
#[ignore] // Skip by default
fn slow_test() { }
}
assert!(condition); // Check boolean
assert_eq!(left, right); // Check equality
assert_ne!(left, right); // Check inequality
cargo test // Run all tests
cargo test -- --nocapture // Show println output
cargo test name // Filter by nameYour turn
Try running the tests with cargo test, see the output with cargo test -- --nocapture, and run the program with cargo run: