Testing

Reviewed & published by Brayan K

Testing in Rust means writing functions marked with the #[test] attribute that use assertion macros to check your code's behaviour, then running them all with cargo test to get a pass/fail report.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll write tests with assert!, assert_eq!, and assert_ne!, organize them in a #[cfg(test)] mod tests, check for panics with #[should_panic], and write tests that return Result.

What You'll Learn in This Lesson

1️⃣ #[test] and the assert Macros

Any function tagged #[test] becomes a test. Inside, use assert_eq!(a, b) to require two values are equal, assert_ne!(a, b) to require they differ, and assert!(cond) for any boolean condition (with an optional custom message). A test passes if it returns without panicking.

// A function we want to test.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// #[test] marks a function as a test. assert_eq! checks two values are equal;
// if they differ, the test fails and prints both. assert! checks a bool.
#[test]
fn adds_positive_numbers() {
    assert_eq!(add(2, 3), 5);
}

#[test]
fn adds_negative_numbers() {
    assert_eq!(add(-4, -6), -10);
    assert_ne!(add(1, 1), 3);      // assert_ne!: must NOT be equal
}

#[test]
fn result_is_even() {
    let sum = add(4, 6);
    assert!(sum % 2 == 0, "expected an even sum, got {}", sum);
}

Each line shows a test name and ok, and the final summary tallies the result. (Tests run in parallel, so the order of the lines can vary between runs.)

2️⃣ #[cfg(test)] mod tests and #[should_panic]

The conventional layout puts tests in a module gated by #[cfg(test)] so they're only compiled when testing, with use super::*; to reach the code under test. To assert that code panics, add #[should_panic] — optionally with expected = "..." to match part of the panic message.

// In real projects tests live in a module gated by #[cfg(test)], so they're
// only compiled when testing. `use super::*;` pulls in the code being tested.
pub fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("cannot divide by zero");
    }
    a / b
}

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

    #[test]
    fn divides_evenly() {
        assert_eq!(divide(10, 2), 5);
    }

    // #[should_panic] passes ONLY if the body panics. `expected` checks the
    // panic message contains that text.
    #[test]
    #[should_panic(expected = "cannot divide by zero")]
    fn panics_on_zero() {
        divide(1, 0);
    }
}

3️⃣ Tests That Return Result

A test may return Result<(), E>. Returning Ok(()) passes and returning Err fails — and crucially, this lets you use the ? operator inside the test to unwrap fallible calls cleanly instead of .unwrap().

// A test can return Result<(), String>. Returning Ok(()) passes; returning Err
// fails. This lets you use the ? operator inside tests.
pub fn parse_port(s: &str) -> Result<u16, String> {
    s.parse::<u16>().map_err(|_| format!("'{}' is not a valid port", s))
}

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

    #[test]
    fn parses_valid_port() -> Result<(), String> {
        let port = parse_port("8080")?;   // ? works because we return Result
        assert_eq!(port, 8080);
        Ok(())
    }

    #[test]
    fn rejects_garbage() {
        assert!(parse_port("abc").is_err());
    }
}

Inside parses_valid_port, the ? after parse_port("8080") would turn an error into a test failure automatically. Because the value parsed fine, the test reaches Ok(()) and passes.

Your turn. Fill in the blank marked ___, then run it.

// 🎯 YOUR TURN — complete the assertion so both tests pass.
pub fn is_even(n: i32) -> bool {
    n % 2 == 0
}

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

    #[test]
    fn four_is_even() {
        ___!(is_even(4), true);   // 👉 the macro that compares two values
    }

    #[test]
    fn seven_is_odd() {
        assert!(!is_even(7));
    }
}

// ✅ Expected output:
//    running 2 tests
//    test tests::four_is_even ... ok
//    test tests::seven_is_odd ... ok
//
//    test result: ok. 2 passed; 0 failed; ...

Mini-Challenge: Test a Reverser

Write a string-reversing function and three tests for it, then run them. Use cargo test and confirm 3 passed.

// 🎯 MINI-CHALLENGE: test a string-reversing function.
//
// 1. Write pub fn reverse(s: &str) -> String that returns s reversed
//    (hint: s.chars().rev().collect()).
// 2. Add a #[cfg(test)] mod tests { use super::*; ... } with three #[test]
//    functions: "rust" reverses to "tsur", "" stays "", and "level" (a
//    palindrome) stays "level". Use assert_eq! in each.
// 3. Run it with cargo test (or rustc --test) and confirm 3 passed.
//
// ✅ Example output:
//    running 3 tests
//    test tests::empty_stays_empty ... ok
//    test tests::palindrome_is_unchanged ... ok
//    test tests::reverses_a_word ... ok
//
//    test result: ok. 3 passed; 0 failed; ...

// your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Testing

ItemPurpose
#[test]mark a test function
assert_eq!(a, b)require equality
#[should_panic]expect a panic
#[cfg(test)] mod teststest-only module
cargo testrun the whole suite

🎉 Lesson Complete!

Practice quiz

Which attribute marks a function as a test?

  • #[testing]
  • #[check]
  • #[test]
  • #[assert]

Answer: #[test]. The #[test] attribute turns a function into a test.

What command runs all tests in a Cargo project?

  • cargo test
  • cargo run
  • cargo check
  • cargo build

Answer: cargo test. cargo test compiles and runs the whole test suite.

What does assert_eq!(a, b) do?

  • Checks a is true
  • Fails if a and b are equal
  • Adds a and b
  • Fails if a and b are not equal

Answer: Fails if a and b are not equal. assert_eq! fails (and prints both values) when a and b differ.

What does assert_ne!(a, b) check?

  • a equals b
  • a and b are NOT equal
  • a is greater than b
  • a is true

Answer: a and b are NOT equal. assert_ne! fails if the two values are equal.

What does assert! take?

  • A single boolean expression
  • Two values
  • A Result
  • A panic

Answer: A single boolean expression. assert! takes one boolean and fails if it is false.

What does #[should_panic] do?

  • Skips the test
  • Fails on panic
  • Passes only if the test body panics
  • Ignores panics

Answer: Passes only if the test body panics. A #[should_panic] test passes only when its body panics.

What does #[cfg(test)] on a module do?

  • Runs it first
  • Compiles it only when building for tests
  • Makes it public
  • Disables it

Answer: Compiles it only when building for tests. #[cfg(test)] compiles the module only during testing, keeping it out of release builds.

Why write use super::*; inside a test module?

  • To import std
  • To enable panics
  • To run faster
  • To bring the parent module's items into scope

Answer: To bring the parent module's items into scope. use super::*; lets the tests call the functions being tested.

What return type lets a test use the ? operator?

  • ()
  • Result<(), E>
  • bool
  • Option<()>

Answer: Result<(), E>. A test returning Result<(), E> can use ?, returning Ok(()) to pass.

Why prefer assert_eq! over assert!(a == b)?

  • It is shorter
  • It is faster
  • On failure it prints both values for easier debugging
  • It never fails

Answer: On failure it prints both values for easier debugging. assert_eq! prints the left and right values when it fails.

Continue this course

Frequently asked questions

How do I write and run a test in Rust?

Mark a function with the #[test] attribute and put your checks inside it using assert macros. In a Cargo project you run all tests with the command cargo test, which compiles a test binary and reports how many passed and failed. Without Cargo you can compile a single file as a test binary with rustc --test file.rs -o t and run ./t. A test passes if it returns normally (or returns Ok); it fails if an assertion fails or the function panics. The summary line 'test result: ok. N passed; 0 failed' tells you everything succeeded.

What is the difference between assert!, assert_eq!, and assert_ne!?

assert! takes a single boolean expression and fails if it is false — use it for any condition. assert_eq! takes two values and fails if they are not equal, printing both the left and right values in the failure message, which makes debugging much easier than a bare boolean. assert_ne! is the opposite: it fails if the two values are equal. Prefer assert_eq! and assert_ne! when comparing two concrete values so you get the helpful diff, and use assert! for more general conditions.

Why put tests inside #[cfg(test)] mod tests?

The #[cfg(test)] attribute means 'only compile this when building for tests', so your test code and any test-only helpers are excluded from normal release builds — they add nothing to your shipped binary. Grouping them in a module named tests is the conventional layout, and the line use super::*; brings the parent module's items into scope so the tests can call the functions being tested. You can have as many test modules as you like, but one mod tests next to the code it exercises is the standard pattern.

How do I test that code panics or returns an error?

For panics, add the #[should_panic] attribute above a #[test] function; the test then passes only if the body panics, and you can add expected = "text" to require that the panic message contains that text. For recoverable errors, write a test that returns Result<(), E>: returning Ok(()) passes and returning Err fails, which also lets you use the ? operator inside the test. Together these cover both kinds of failure: unrecoverable panics and ordinary Result errors.