Skip to content

Expert · Testing, tooling and unsafe Rust · topic 39 of 50 · 30 min

Unit tests

Explanation

A unit test checks one small piece of code. In Rust you write tests in a #[cfg(test)] mod tests block in the same file, mark each test function with #[test], and check results with assert!, assert_eq! and assert_ne!. cargo test compiles and runs them all; a panic means the test failed.

Why test?

The compiler catches type and memory mistakes, but not logic mistakes: a GST rate of 1.8 instead of 18, or a discount applied twice. A test is code that runs your code with known inputs and checks the outputs. Once written, tests run in seconds every time you change something, so you notice breakage immediately instead of hearing about it from a customer.

Think of it like this: Tests are like the quality check at a mobile phone factory. Every phone is switched on, the camera is tested and the speaker is played before it is packed. Nobody trusts "it worked last week"; the checks run on every phone, every time, quickly and automatically.

Your first test

rust
pub fn gst(amount: u32, rate: u32) -> u32 {
    amount * rate / 100
}

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

    #[test]
    fn gst_at_18_percent() {
        assert_eq!(gst(1000, 18), 180);
    }

    #[test]
    fn gst_on_zero_is_zero() {
        assert_eq!(gst(0, 18), 0);
    }
}
Tests usually live at the bottom of the same file as the code they test.
  • #[cfg(test)] means "compile this module only when running tests", so tests do not make your real program bigger.
  • use super::*; brings everything from the parent module (the code under test) into the tests module.
  • #[test] marks a function as a test. It takes no arguments.
  • assert_eq!(left, right) fails the test if the two values differ, and prints both.
bash
cargo test
cargo test gst              # only tests whose name contains "gst"
cargo test -- --nocapture   # show println! output from passing tests
Arguments after -- go to the test runner, not to cargo.
output
running 2 tests
test tests::gst_at_18_percent ... ok
test tests::gst_on_zero_is_zero ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Typical output. Tests run in parallel threads by default.

The assert macros

MacroFails when
assert!(cond)cond is false
assert_eq!(a, b)a != b (prints both values)
assert_ne!(a, b)a == b
assert!(cond, "msg {}", x)cond is false, with your message

assert_eq! needs the values to implement PartialEq (to compare) and Debug (to print them on failure). Derive both on your own types.

Testing failures

rust
pub fn divide_bill(total: u32, people: u32) -> u32 {
    if people == 0 {
        panic!("people must be at least 1");
    }
    total / people
}

#[test]
#[should_panic(expected = "at least 1")]
fn zero_people_panics() {
    divide_bill(900, 0);
}

#[test]
fn parse_ok() -> Result<(), std::num::ParseIntError> {
    let n: u32 = "42".parse()?;
    assert_eq!(n, 42);
    Ok(())
}
should_panic passes only if the code panics with a message containing the expected text. A test can also return Result and use ?.

Good test habits

  • Name tests after the behaviour: refund_is_zero_after_30_days, not test1.
  • Test the edges: zero, one, the maximum, empty strings, Hindi text, negative cases.
  • One idea per test, so a failure tells you exactly what broke.
  • Use #[ignore] for very slow tests and run them with cargo test -- --ignored.
  • Private functions can be tested too, because the tests module is a child of the module.

Common mistakes

  • Forgetting use super::*; and getting "cannot find function in this scope".
  • Comparing floats with assert_eq!: 0.1 + 0.2 is not exactly 0.3. Check that the difference is tiny instead.
  • Tests that depend on each other or on run order. Tests run in parallel; each must set up its own data.
  • Not running tests before pushing. Make cargo test part of your routine (and of CI, next topics).

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Unit tests · 1/6

Compiler: types and memory Tests: logic

Tests catch logic mistakes the compiler cannot see, like a wrong tax rate.

Prefer a video by a teacher? Find free videos on “rust unit testing cargo test assert_eq should_panic” on YouTube ↗

In real life: Protecting an EMI formula

A loan app's EMI function is used on every product page. The team writes unit tests with known values from the bank's own calculator, plus edge cases like a zero interest rate (which would divide by zero in the naive formula). When a developer later "simplifies" the function and breaks the zero-rate case, cargo test fails on their laptop before the change ever reaches customers.

rust
pub fn emi(principal: f64, yearly_rate: f64, months: u32) -> f64 {
    if yearly_rate == 0.0 {
        return principal / months as f64;
    }
    let r = yearly_rate / 12.0 / 100.0;
    let f = (1.0 + r).powi(months as i32);
    principal * r * f / (f - 1.0)
}

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

    #[test]
    fn zero_rate_splits_evenly() {
        assert_eq!(emi(12_000.0, 0.0, 12), 1_000.0);
    }

    #[test]
    fn known_value() {
        let v = emi(500_000.0, 9.0, 60);
        assert!((v - 10_379.18).abs() < 0.5, "got {v}");
    }
}
Floats are compared with a small tolerance.

Practice (0/3 done)

Task 1 · Question

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

Task 2 · Question

This test fails. Why? #[test] fn sum() { assert_eq!(0.1 + 0.2, 0.3); }

Task 3 · Practice

Write fn grade(marks: u32) -> char (A 90+, B 75+, C 40+, F otherwise; panic if marks > 100). Write at least 6 unit tests: each grade, the boundaries 90, 75 and 40, and a should_panic test for 101. Run cargo test and make one test fail on purpose to see the output.

Quick quiz

Question 1

1.Which attribute marks a function as a test?

Question 2

2.What makes a test fail?

Question 3

3.Which command shows println! output from passing tests?

Question 4

4.What does assert_eq! need from the compared type?

Question 5

5.Can unit tests call private functions of their module?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which attribute marks a function as a test?

Answer: #[test]

#[test] marks individual test functions.

What makes a test fail?

Answer: A panic (for example a failed assert)

Failed asserts panic, and a panic fails the test.

Which command shows println! output from passing tests?

Answer: cargo test -- --nocapture

--nocapture is passed to the test runner after --.

What does assert_eq! need from the compared type?

Answer: PartialEq and Debug

It compares with == and prints both sides on failure.

Can unit tests call private functions of their module?

Answer: Yes, because the tests module is a child module

Child modules can see private items of their parent.

What does a unit test usually check?Related

Answer: One small piece of code, like a single function

A unit test checks one small unit, usually a function, without a network or a real database.

From Unit testing: checking one piece at a time

Which is a C unit-testing framework popular in embedded work?Related

Answer: Unity

Unity is a small C framework.

From assert, logging and testing C code

Where does a cargo project keep its main code?Related

Answer: src/main.rs

cargo new puts the starting code in src/main.rs.

From Your first program and cargo

Why should unit tests avoid DateTime.Now?Related

Answer: The result changes over time, so tests become unrepeatable

Tests must give the same result every run.

From Unit testing with xUnit

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Testing, tooling and unsafe Rust” before its exam: Integration tests and doc tests, Tooling: rustfmt, Clippy, docs and editions, Unsafe Rust: what it is and is not.
  2. Module 10 exam: Testing, tooling and unsafe Rust · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Real projects: CLI, web and WebAssembly. 50 topics left in the course before the final exam.

← Previous: Async Rust with tokio