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
- Marking a function with #[test]
- assert!, assert_eq!, and assert_ne!
- Grouping tests in #[cfg(test)] mod tests
- Checking panics with #[should_panic]
- Writing tests that return Result
- Reading the cargo test summary
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 hereCommon Errors (and the fix)
- ❌ Test never runs — you forgot the #[test] attribute, so it's just a normal function. ✅ Fix: add #[test] directly above it.
- ❌ cannot find function `divide` in this scope inside the test module — missing import. ✅ Fix: add use super::*;.
- ❌ the `?` operator can only be used in a function that returns Result — your test returns (). ✅ Fix: make it return Result<(), String> and end with Ok(()).
- ❌ A #[should_panic] test that fails — the code didn't panic, or panicked with a different message. ✅ Fix: check the expected text matches the real panic.
Pro Tips
- 💡 Prefer assert_eq! over assert!(a == b) — on failure it prints both values for you.
- 💡 Run one test with cargo test name; it filters by substring of the test's name.
- 💡 See println! output from passing tests with cargo test -- --nocapture.
📋 Quick Reference — Testing
| Item | Purpose |
|---|---|
| #[test] | mark a test function |
| assert_eq!(a, b) | require equality |
| #[should_panic] | expect a panic |
| #[cfg(test)] mod tests | test-only module |
| cargo test | run the whole suite |
🎉 Lesson Complete!
- ✅ #[test] turns a function into a test; cargo test runs them all
- ✅ assert_eq! / assert_ne! / assert! express your checks
- ✅ #[cfg(test)] mod tests keeps tests out of release builds
- ✅ #[should_panic] verifies panics; Result<(), E> tests enable ?
- ✅ The summary test result: ok. N passed; 0 failed confirms success
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
- Previous: The Default & Clone Traits
- Next: File I/O (std::fs) — Read and write files with fs, BufReader, and the ? operator
- Quick reference: Rust cheat sheet
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.