Expert · Testing, tooling and unsafe Rust · topic 39 of 50 · 30 min
Unit tests
Explanation
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.
Your first test
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);
}
}- #[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.
cargo test
cargo test gst # only tests whose name contains "gst"
cargo test -- --nocapture # show println! output from passing testsrunning 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 outThe assert macros
| Macro | Fails 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
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(())
}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
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.
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}");
}
}Practice (0/3 done)
What does #[cfg(test)] on the tests module do?
This test fails. Why? #[test] fn sum() { assert_eq!(0.1 + 0.2, 0.3); }
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Integration tests and doc testsTesting, tooling and unsafe Rust · same module
- Tooling: rustfmt, Clippy, docs and editionsTesting, tooling and unsafe Rust · same module
- Unsafe Rust: what it is and is notTesting, tooling and unsafe Rust · same module
- Unit testing: checking one piece at a timeBackend Development · Testing and API documentation
- assert, logging and testing C codeC Programming (from zero to systems) · Debugging and tools
- Unit testing with xUnitC# and .NET · Testing, tooling and Unity
Questions people ask about this
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.
Which is a C unit-testing framework popular in embedded work?Related
Where does a cargo project keep its main code?Related
Answer: src/main.rs
cargo new puts the starting code in src/main.rs.
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.
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
- 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.
- Module 10 exam: Testing, tooling and unsafe Rust · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Real projects: CLI, web and WebAssembly. 50 topics left in the course before the final exam.