Step 4 of 6
Testing failures: should_panic and Result
Code that rejects bad input is just as important to test as code that handles good input. Rust gives you two tools, matching the two ways code reports failure.
#[should_panic]: the code is supposed to panic
Some functions panic on misuse: indexing past the end, or a withdraw that refuses to overdraw. Add #[should_panic] under #[test], and the test passes only if the code panics. Add expected = "..." to also check the panic message contains that text, so the test doesn't pass by accident because of some other panic:
#[test]
#[should_panic(expected = "insufficient funds")]
fn spending_more_than_the_balance_panics() {
let mut wallet = Wallet { balance: 10 };
wallet.spend(25);
}
In the cargo test output, these tests are marked: test tests::spending_more_than_the_balance_panics - should panic ... ok.
Tests that return Result
Most Rust code reports errors with Result. You can check those with plain asserts (assert!(r.is_err()), assert_eq!(r, Ok(250))), and a test function may also return Result<(), E> itself. Then you can use ? inside it: if any step returns Err, the test fails with that error. It reads like normal code instead of a pile of .unwrap()s:
#[test]
fn parses_and_spends() -> Result<(), String> {
let amount = parse_amount("250")?; // an Err here fails the test
let mut wallet = Wallet { balance: 1000 };
wallet.spend(amount);
assert_eq!(wallet.balance, 750);
Ok(())
}
The same checks work in a normal program, so here they are, running:
fn parse_amount(s: &str) -> Result<u64, String> {
let n: u64 = s.trim().parse().map_err(|_| format!("not a number: {s:?}"))?;
if n == 0 {
return Err(String::from("amount must be positive"));
}
Ok(n)
}
fn main() {
println!("{:?}", parse_amount("250"));
println!("{:?}", parse_amount("0"));
println!("{:?}", parse_amount("ten"));
assert!(parse_amount("-3").is_err());
assert!(matches!(parse_amount(" 7 "), Ok(7)));
println!("checks passed");
}
Ok(250)
Err("amount must be positive")
Err("not a number: \"ten\"")
checks passed
matches!(value, pattern) (from the enums module) is handy in tests when you care about the shape of a value but not every detail, like "it's an Err of any kind".
Ignoring slow tests
#[ignore] under #[test] skips a test in a normal run (it shows as ignored). Run those with cargo test -- --ignored. Teams use it for tests that are slow or need a network.
Common mistakes
#[should_panic]withoutexpected: the test passes on any panic, including an unrelatedunwraponNonein your setup code.- Using
#[should_panic]for functions that returnResult: they don't panic on bad input, they returnErr. Assert on theResultinstead. - Forgetting
Ok(())at the end of a test that returnsResult:error[E0308]: mismatched types.
Your task
Write tests for Wallet::spend and parse_amount:
- A
#[should_panic(expected = "insufficient funds")]test that spends more than the balance. - A test that spending the whole balance leaves exactly 0.
- A test that returns
Result<(), String>and uses?onparse_amountwith an amount that has spaces around it (like" 250 "). - A test that
parse_amount("0")is an error.
Challenge 2
parse_hex_color reads colors like #ff8000 into (red, green, blue). It needs the #, exactly 6 hex digits, and returns Err otherwise. Write at least 3 tests; at least one must return Result<(), String> and use ?.
Challenge 3
A Grid stores width * height cells row by row. get(row, col) panics with a message containing out of bounds for a bad position, and try_get returns None instead. Write:
- a
#[should_panic(expected = "out of bounds")]test forget, - at least 2 more tests: that
getreads the right cell in a grid that is not square, and thattry_getreturnsNoneoutside the grid andSomeinside.
Previous: Tests that catch bugs Next: Fixing bugs test-first