🧪 Unit Testing in Rust: Your Code’s Safety Net
Universal Analogy: Think of unit tests like a quality inspector at a toy factory. Before any toy goes to the store, the inspector checks each part: Does the wheel spin? Does the button click? Does the battery compartment close properly? Unit tests do the same for your code—they check each small piece to make sure it works perfectly!
🎯 What Are Unit Tests?
Imagine you’re building a LEGO castle. Would you wait until you’ve placed all 1000 pieces to check if the foundation is stable? Of course not! You’d check as you build.
Unit tests are small programs that test tiny pieces of your code—like checking one LEGO brick at a time.
Why Do We Need Them?
graph TD A["Write Code"] --> B["Write Tests"] B --> C{Tests Pass?} C -->|Yes| D["✅ Code Works!"] C -->|No| E["🔧 Fix the Bug"] E --> B
Real Life Example:
- You write a function that adds two numbers
- Unit test checks: Does 2 + 2 = 4? ✅
- Unit test checks: Does 0 + 0 = 0? ✅
- If any check fails, you know exactly where the problem is!
🏷️ The #[test] Attribute: Marking Your Tests
In Rust, you tell the computer “Hey, this function is a test!” by adding a special label called #[test].
Think of it like putting a “QUALITY CHECK” sticker on a toy. The factory knows: this isn’t a toy to sell—it’s a checker!
Simple Example
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[test]
fn test_add_works() {
let result = add(2, 3);
assert_eq!(result, 5);
}
What’s happening:
- We have a real function
addthat adds numbers - Below it,
#[test]says “this next function is a test” - The test calls
add(2, 3)and checks if it equals5
Where Do Tests Live?
// Your regular code
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
// Tests go in a special section
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiply() {
assert_eq!(multiply(3, 4), 12);
}
}
The #[cfg(test)] label tells Rust: “Only compile this code when running tests.” It’s like having a secret testing room in the factory that visitors never see!
✅ Test Assertions: The Inspector’s Checklist
Assertions are the actual checks your inspector performs. Rust gives you several tools:
1. assert! - Is This True?
The simplest check. It asks: “Is this statement true?”
#[test]
fn test_is_positive() {
let number = 5;
assert!(number > 0);
// Passes because 5 > 0 is TRUE
}
Like asking: “Is this toy red?” ✅ Yes → Pass!
2. assert_eq! - Are These Equal?
Checks if two things are exactly the same.
#[test]
fn test_greeting() {
let greeting = "Hello";
assert_eq!(greeting, "Hello");
// Passes: both are "Hello"
}
Like asking: “Does this box have 10 crayons?” You count: 10. ✅ Match!
3. assert_ne! - Are These Different?
Checks that two things are NOT the same.
#[test]
fn test_unique_ids() {
let id1 = generate_id();
let id2 = generate_id();
assert_ne!(id1, id2);
// Passes if each ID is unique
}
Like asking: “Are these two snowflakes different?” ❄️ ≠ ❄️ → Pass!
Custom Error Messages
When a test fails, you want to know WHY. Add messages!
#[test]
fn test_with_message() {
let age = 15;
assert!(
age >= 18,
"Expected adult, got age {}",
age
);
}
This fails and tells you: “Expected adult, got age 15”
💥 The #[should_panic] Attribute: Testing Failures
Sometimes you WANT your code to crash! Like a smoke detector—it SHOULD scream when there’s fire.
Basic Usage
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Cannot divide by zero!");
}
a / b
}
#[test]
#[should_panic]
fn test_divide_by_zero_panics() {
divide(10, 0);
// This SHOULD crash, so test passes!
}
The logic:
- We call
divide(10, 0) - The function panics (crashes) ✅
- Because we said
#[should_panic], a crash = SUCCESS
Be Specific: Check the Error Message
What if code panics for the WRONG reason? Be specific:
#[test]
#[should_panic(expected = "divide by zero")]
fn test_specific_panic() {
divide(10, 0);
// Only passes if panic message
// contains "divide by zero"
}
Like telling the inspector: “This toy should break, but ONLY if you press the red button—not the blue one!”
📦 Using Result in Tests: Pass or Fail, Explained
Sometimes instead of panicking, you want tests to return success or an error message—like a report card!
Basic Result Test
#[test]
fn test_with_result() -> Result<(), String> {
let value = 10;
if value > 5 {
Ok(()) // Test passed!
} else {
Err(String::from("Value too small"))
}
}
How it works:
- Return
Ok(())→ Test passes ✅ - Return
Err("message")→ Test fails with your message ❌
Why Use Result Instead of Assert?
graph TD A["Choose Test Style"] --> B{Need error details?} B -->|Yes| C["Use Result"] B -->|No| D{Expect panic?} D -->|Yes| E["Use should_panic"] D -->|No| F["Use assert!"]
Result is great when:
- You’re testing functions that already return
Result - You want cleaner error messages
- You’re using the
?operator
Using the ? Operator
#[test]
fn test_file_reading() -> Result<(), Box<dyn std::error::Error>> {
let content = std::fs::read_to_string("test.txt")?;
assert!(content.contains("hello"));
Ok(())
}
The ? automatically fails the test with a helpful message if reading fails!
🏃 Running Your Tests
Open your terminal and type:
cargo test
What you’ll see:
running 3 tests
test tests::test_add ... ok
test tests::test_multiply ... ok
test tests::test_divide_by_zero ... ok
test result: ok. 3 passed; 0 failed
🎉 All green = All good!
Helpful Commands
| Command | What It Does |
|---|---|
cargo test |
Run all tests |
cargo test add |
Run tests with “add” in name |
cargo test -- --nocapture |
Show print statements |
🎨 Putting It All Together
Here’s a complete example with all concepts:
// The code we're testing
fn validate_age(age: i32) -> Result<String, String> {
if age < 0 {
panic!("Age cannot be negative!");
}
if age < 18 {
Err(String::from("Too young"))
} else {
Ok(String::from("Welcome!"))
}
}
#[cfg(test)]
mod tests {
use super::*;
// Using assert_eq!
#[test]
fn adult_gets_welcome() {
let result = validate_age(25);
assert_eq!(result, Ok(String::from("Welcome!")));
}
// Using assert!
#[test]
fn teenager_is_rejected() {
let result = validate_age(15);
assert!(result.is_err());
}
// Using should_panic
#[test]
#[should_panic(expected = "negative")]
fn negative_age_panics() {
validate_age(-5);
}
// Using Result
#[test]
fn test_returns_result() -> Result<(), String> {
match validate_age(20) {
Ok(msg) if msg == "Welcome!" => Ok(()),
_ => Err(String::from("Unexpected result"))
}
}
}
🌟 Key Takeaways
graph TD A["Unit Testing Basics"] --> B["#[test] marks test functions"] A --> C["Assertions check conditions"] A --> D["#[should_panic] expects crashes"] A --> E["Result gives detailed pass/fail"] C --> C1["assert!#40;condition#41;"] C --> C2["assert_eq!#40;a, b#41;"] C --> C3["assert_ne!#40;a, b#41;"]
| Concept | When to Use | Example |
|---|---|---|
#[test] |
Every test function | #[test] fn my_test() |
assert! |
Check if true | assert!(x > 0) |
assert_eq! |
Check equality | assert_eq!(2+2, 4) |
#[should_panic] |
Expect a crash | Testing error cases |
Result |
Detailed errors | File operations |
💪 You Did It!
You now understand:
- ✅ What unit tests are and why they matter
- ✅ How to mark functions as tests with
#[test] - ✅ Three types of assertions:
assert!,assert_eq!,assert_ne! - ✅ How to test code that SHOULD crash with
#[should_panic] - ✅ How to use
Resultfor cleaner test outcomes
Remember: Tests are your code’s best friend. They catch bugs before users do. Write tests, sleep peacefully! 🛏️✨
