Unit Testing

Back

Loading concept...

🧪 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:

  1. We have a real function add that adds numbers
  2. Below it, #[test] says “this next function is a test”
  3. The test calls add(2, 3) and checks if it equals 5

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["&#35;[test] marks test functions"] A --> C["Assertions check conditions"] A --> D["&#35;[should_panic] expects crashes"] A --> E["Result gives detailed pass/fail"] C --> C1["assert!&#35;40;condition&#35;41;"] C --> C2["assert_eq!&#35;40;a, b&#35;41;"] C --> C3["assert_ne!&#35;40;a, b&#35;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 Result for cleaner test outcomes

Remember: Tests are your code’s best friend. They catch bugs before users do. Write tests, sleep peacefully! 🛏️✨

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.