Last lesson ended with a quiet observation about your own habits: every single time you've changed a piece of code in this course, you've checked that it still works by running it and reading the output, by hand. Today that habit gets handed off to the computer — and as a bonus, almost every example in this lesson is code you already wrote, several lessons ago, getting checked by a machine for the first time.
The smallest possible test
Here's the shape every Rust test grows from — the same shape cargo new --lib writes for you automatically the moment you create a new project:
Four pieces, and every one of them earns its place:
#[test]marksit_worksas a test function.cargo testfinds every function in your project carrying this attribute, runs each one in isolation, and reports pass or fail.#[cfg(test)] mod tests—modis short for "module," Rust's way of bundling related code under one name (the full story is a couple of lessons away), and#[cfg(test)]means "only include this in the build when running tests." The programcargo buildproduces never even sees this code; it exists only whencargo testruns.use super::*brings everything from the parent module — the rest of this file — into scope insidetests. Without it,addwouldn't be visible in here at all.assert_eq!(result, 4)is the actual check: "I am asserting these two values are equal. If they're not, stop this test right here and tell me exactly how."
That exact pattern — a #[cfg(test)] mod tests block, sitting at the bottom of the same file as the code it tests, with use super::* at the top — is so universal that you'll see it in nearly every Rust source file that has tests at all.
Running it
cargo test compiles your project in test mode and runs every #[test] function it finds. A passing run looks like this:
One line per test — test tests::it_works ... ok — followed by a summary. Now go change that 4 to a 5 and run it again. assert_eq! doesn't print a polite warning and move on. It panics — the exact same panic! mechanism from the panic-vs-Result lesson — with a message naming both sides of the comparison: assertion `left == right` failed, then left: 4 and right: 5. cargo test catches that panic for you, marks the test FAILED instead of letting your whole program crash, runs every other test anyway, and shows you the message. Every test failure you will ever see in Rust is, underneath, that same panic! — assert_eq! is just a convenient way of triggering it for you, with a message that writes itself.
Real code, real tests: largest
Time for the victory lap. Here's largest<T: PartialOrd>, from the generics lesson — same function, same two vectors from its main, three lessons ago — now as actual tests:
One new piece of syntax: that * in front of largest(&numbers). largest returns &T — a reference, not a value. assert_eq!(largest(&numbers), 100), without the *, would be comparing a &i32 to an i32 — two different types — and wouldn't compile at all. * is the dereference operator from the references-and-borrowing lesson: "follow this reference, and give me the value sitting at the other end." Two tests, two completely different types, one generic function — exactly the promise the generics lesson made, now checked by a machine instead of a println! you'd have had to read yourself.
assert! — and a bonus from the traits lesson
assert_eq! checks that two things are equal. Its mirror image, assert_ne!, checks they're not equal — same machinery, opposite question. And their plainer relative, assert!, checks one single thing: is this expression true? Here's a test for CountError's Display implementation, from the traits lesson two lessons back — with a small bonus hiding inside it:
"not a number".parse::<i32>() fails, exactly the way you'd expect — and .unwrap_err() is .unwrap()'s mirror image: where .unwrap() reaches into Ok and panics on Err, .unwrap_err() reaches into Err and panics on Ok. Here it pulls out the ParseIntError directly, just so the test has one to build a CountError::Parse around.
Then — err.to_string(). You never wrote a to_string method on CountError. Nobody did. The moment a type implements Display — which CountError did, two lessons ago — the standard library gives it .to_string() automatically, for free. One more thing impl fmt::Display for CountError was quietly buying you, on top of everything println! could already do with it. And assert!(...) is the plain form doing its job: not "are these equal," just "is this true" — here, "does the error message contain the phrase a person reading it would expect to find."
When code is supposed to panic
Some code panics on purpose. Guess::new, from the panic-vs-Result lesson, panics the instant it's handed a number outside 1 to 100 — and testing that it does is just as direct as testing that something doesn't:
#[should_panic] flips the usual rule on its head: this test PASSES if Guess::new(200) panics, and FAILS if it doesn't. The expected = "..." part narrows it further — the panic has to happen, and its message has to contain this text. Without expected, any panic anywhere inside Guess::new — even one caused by a completely unrelated bug — would make this test pass for the wrong reason. With it, this test checks one specific thing: calling Guess::new(200) produces this panic, with this message — the exact words you wrote, by hand, lessons ago.
Returning Result from a test
One last shape, and it should look familiar. A test function can return Result<(), E> instead of nothing — and the moment it does, ? works inside it, exactly like every function since the propagating-errors lesson:
Let ? produce an Err, or return one yourself, and the test fails — with that error printed for you to read, no unwrap() required. Return Ok(()), and it passes. This is the entire ?-propagation system from a few lessons ago, applied to a test function instead of main: a test is just a function, and everything you already know about functions that return Result applies to it without a single exception.
Quick exercise
Two ways to put this to work immediately, on code you've already written:
- Pick any function from an earlier lesson you're confident about. Write a
#[test]for it. Runcargo testand watch it pass. Then deliberately break the function — flip a>to a<, a+to a-— and runcargo testagain. Watch it fail, read theleft/rightvalues in the message, and fix it back. That red-then-green cycle is the actual day-to-day rhythm of testing, and you've now done it with your own hands. - Write a
#[should_panic]test for the very first panic this course ever showed you: aVecwith three elements, indexed at position 10. The message will start withindex out of bounds— that's allexpectedneeds to match.
Stop and notice what just happened across this lesson: every test exercised code from a different earlier lesson — generics, traits, error handling, even the very first panic! you ever triggered — and each one passed or failed with a message telling you exactly why, in seconds, without you reading a line of console output by hand. That's the whole point. That's testing.
Twice now, you've handed a function's name to something that wanted to call it later — or_insert_with(Vec::new), back in the hash-maps lesson, and .map_err(CountError::Io), in the custom-error-types lesson. Both worked because a function is just a value you can pass around, the same as any other. The next lesson introduces the other way to make one of those: writing a small, unnamed function inline, on the spot — often just a handful of characters — that can reach out and use variables from the code surrounding it. They're called closures, and once you have them, code that loops over a Vec or builds up a HashMap gets dramatically shorter: closures.