Skip to content

Rust Testing

Rust has testing built directly into the language and cargo, so you don’t need to reach for a separate library to get started. This page covers just enough to write and run your first tests.

A test is a regular function marked with #[test]:

fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
}

A few things worth pointing out:

  • #[cfg(test)] tells Rust this module is only compiled when running tests — it’s not part of your normal program.
  • use super::*; brings in everything from the parent module (here, the add function), so the test can call it.
  • #[test] marks a specific function as a test that cargo test should run.

From your project folder:

Terminal window
cargo test

You’ll see output like:

running 1 test
test tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignored

Three macros cover most of what you’ll need:

#[test]
fn checks() {
assert!(2 + 2 == 4); // fails the test if the condition is false
assert_eq!(2 + 2, 4); // fails if the two values aren't equal
assert_ne!(2 + 2, 5); // fails if the two values ARE equal
}

assert_eq! and assert_ne! are usually preferred over a plain assert!(a == b) — when they fail, they print both values, so you can immediately see what didn’t match instead of just “assertion failed.”

fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_will_fail() {
assert_eq!(add(2, 2), 5);
}
}
test tests::this_will_fail ... FAILED
assertion `left == right` failed
left: 4
right: 5

That’s exactly the kind of message you want when a test fails — it tells you what your code actually produced versus what the test expected.

Sometimes the correct behavior is a panic — say, a function that should refuse invalid input. #[should_panic] tests for that:

fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("cannot divide by zero");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn dividing_by_zero_panics() {
divide(10, 0);
}
}

When a test fails, you want to know why as fast as possible. Two small additions help a lot.

A custom message. assert!, assert_eq! and assert_ne! all accept an extra message after the values, written just like a println!. It’s printed only if the check fails.

expected on should_panic. A plain #[should_panic] passes for any panic, even one caused by a different bug. Adding expected = "..." makes the test pass only if the panic message contains that text.

Here’s the divide function again, with a main so you can run it, and two tests that use both ideas:

fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("cannot divide by zero");
}
a / b
}
fn main() {
println!("10 / 2 = {}", divide(10, 2));
println!("7 / 2 = {}", divide(7, 2));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounds_down() {
let result = divide(7, 2);
assert_eq!(result, 3, "7 / 2 should round down, but got {result}");
}
#[test]
#[should_panic(expected = "divide by zero")]
fn dividing_by_zero_panics() {
divide(10, 0);
}
}

Output:

10 / 2 = 5
7 / 2 = 3
▶ Try it Yourself

The output above is what cargo run prints, because main doesn’t run the tests. To run the tests, use cargo test. In the Rust Playground, open the menu next to Run and pick Test. You’ll see:

running 2 tests
test tests::rounds_down ... ok
test tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Tests run in parallel, so the two test ... lines may show up in either order. Now imagine the test expected 4 instead of 3. The failure would include your message, right next to the two values:

assertion `left == right` failed: 7 / 2 should round down, but got 3
left: 3
right: 4

And if divide panicked with a different message, the expected check catches it:

note: panic did not contain expected string
panic message: "cannot divide by zero"
expected substring: "negative"

The honest answer: for a short script, you probably won’t. Tests start paying off once code is going to be reused or changed later — they let you refactor with confidence, because cargo test will tell you immediately if you broke something, instead of finding out from a user.

Tests can also live in your documentation: code examples inside /// doc comments run with cargo test too, so your examples can never go out of date. The Documentation page shows how.

For both exercises, the solution has a main you can run, and tests you run with cargo test (or Test in the Playground).

Write fn grade(score: u32) -> char that returns 'A' for 90 and up, 'B' for 80 to 89, 'C' for 70 to 79, and 'F' for anything lower. Then write tests that check the edges, like 90 and 89. Hint: edges are where bugs hide. A test for exactly 90 catches the classic mistake of writing > instead of >=.

Show solution
fn grade(score: u32) -> char {
if score >= 90 {
'A'
} else if score >= 80 {
'B'
} else if score >= 70 {
'C'
} else {
'F'
}
}
fn main() {
for score in [95, 85, 72, 40] {
println!("{score} -> {}", grade(score));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edges_of_a() {
assert_eq!(grade(90), 'A');
assert_eq!(grade(89), 'B');
}
#[test]
fn edges_of_c() {
assert_eq!(grade(70), 'C');
assert_eq!(grade(69), 'F');
}
#[test]
fn zero_is_f() {
assert_eq!(grade(0), 'F', "a score of 0 should fail");
}
}

Output:

95 -> A
85 -> B
72 -> C
40 -> F
▶ Try it Yourself

cargo test should report 3 passed. Try changing score >= 90 to score > 90 and run the tests again: edges_of_a fails and shows you left: 'B' and right: 'A'.

Write fn safe_divide(a: i32, b: i32) -> Option<i32> that returns None when b is 0 instead of panicking. Write one test for a normal division and one for dividing by zero. Hint: assert_eq! works with Option too, so you can write assert_eq!(safe_divide(1, 0), None).

Show solution
fn safe_divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
fn main() {
println!("{:?}", safe_divide(10, 2));
println!("{:?}", safe_divide(1, 0));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn divides_normally() {
assert_eq!(safe_divide(10, 2), Some(5));
}
#[test]
fn zero_gives_none() {
assert_eq!(safe_divide(1, 0), None);
}
}

Output:

Some(5)
None
▶ Try it Yourself

Compare this with the divide function above. There, dividing by zero is a panic, so the test needs #[should_panic]. Here it’s just a normal return value, so a plain assert_eq! is enough. Functions that return Option or Result are usually easier to test.

  • Mark a function #[test] to make it a test; run all tests with cargo test.
  • assert!, assert_eq!, and assert_ne! cover most everyday checks — prefer assert_eq!/assert_ne! for better failure messages.
  • #[should_panic] tests that a function panics when it’s supposed to. Add expected = "..." to check it panicked for the right reason.
  • Add a custom message to any assert macro to explain what went wrong when it fails.
  • Tests are most valuable for code you expect to change or reuse later.

Quick check

1. What attribute marks a function as a test that cargo test should run?

2. Why are assert_eq! and assert_ne! usually preferred over a plain assert!(a == b)?

3. What does #[should_panic] do?

Score: 0 / 3