14.1Introduction to testing
You've tested every program in this course by running it and looking at the output. That works for a ten-line example and fails completely for anything real: you can't re-check by hand, every time you make a change, that all the old behavior still works. Automated tests are code that checks your code, run by a single command, and Rust takes them seriously enough to build a test framework into the language itself. This chapter teaches testing as a habit, because in Rust it's not an external tool you bolt on; it's cargo test, there from the first day of any project.
Why test
The case for tests is really a case against a specific fear: the fear of changing working code. Without tests, every edit to a program of any size carries the worry that you've silently broken something elsewhere, the exact silent-wrong-answer failure mode lesson 1.7 said Rust works to eliminate. The compiler catches type errors and the borrow checker catches memory errors, but neither can tell you that your calculate_tax function now returns the wrong number. Only a test that knows the right answer can.
A good test suite turns "I hope I didn't break anything" into "the tests pass, so I didn't break anything I have a test for." It lets you refactor fearlessly (rename, restructure, optimize) and confirm in seconds that behavior is unchanged. It documents what the code is supposed to do, in runnable form. And it catches regressions: a bug you fix once, with a test added, stays fixed, because the test fails the day someone reintroduces it (the regression-check step from the debugging process in lesson 3.2, automated).
Key insight
Tests buy you the confidence to change code. The compiler proves your program is type-safe and memory-safe; it cannot prove your program is correct, because only you know what correct means. A test encodes a piece of that knowledge ("given this input, the answer is that") so the machine can check it forever after. The more tests you have, the larger the share of "is it still correct?" the machine answers for you.
What makes a good test
A test does three things, traditionally remembered as arrange, act, assert. It arranges the inputs and any setup, acts by calling the code under test, and asserts that the result matches what you expect. The assertion is the heart of it: a test without a check isn't a test, just code that runs.
Good tests share a few qualities. They're focused: each one checks a single behavior, so when it fails you know what broke (the same one-concept discipline these lessons follow). They cover the edge cases, not just the easy path: zero, empty input, the maximum value, the boundary between two ranges, the cases where bugs hide. They're independent: one test passing or failing doesn't depend on another, so they can run in any order. And they're fast, so you run them constantly rather than dreading them.
The flip side, what to test: not every line needs a test, but every piece of logic that could be wrong benefits from one. A function that does a real computation, handles a tricky input, or enforces a rule is worth testing. Trivial glue code is not. As lesson 3.6 put it, the goal is to find issues before they become problems, and a test is how you pin a "this works" claim down permanently.
Testing is built in
Here's what makes Rust different from many languages: you don't choose and install a testing library, configure a test runner, and wire it into your build. It's already there. Write a function, mark it #[test], run cargo test, and the built-in framework finds it, runs it, and reports pass or fail. The next lesson writes the first one. Because testing ships with the toolchain, every Rust project tests the same way, every tutorial and library uses the same #[test] and assert_eq!, and there's no debate about which framework to adopt. The friction that keeps people from writing tests in other ecosystems mostly isn't here.
This is also why lesson 13.5 spent time on the library/binary split. The logic you put in lib.rs is the logic you can test thoroughly, both from inside (unit tests, lesson 14.3) and from outside through its public API (integration tests, lesson 14.4). The two chapters are partners: organize code so it's testable, then test it.
Quiz time
Question #1
The compiler already checks types and the borrow checker checks memory safety. What kind of error can only a test catch?
Show solution
A logic error: code that compiles and is memory-safe but produces the wrong answer (a calculate_tax that returns the wrong number, an off-by-one, a wrong operator). The compiler can't know what "correct" means for your problem; only a test that encodes the expected result can verify it. This is the logic-error category from lesson 3.1.
Question #2
Name the three steps a typical test follows, and which one is non-negotiable.
Show solution
Arrange (set up inputs), act (call the code under test), assert (check the result against what's expected). The assertion is non-negotiable: a test that runs code but never checks the outcome isn't really a test, it just confirms the code didn't crash.
Question #3
Why does Rust's built-in test framework make testing more likely to actually happen than in ecosystems where you install a separate one?
Show solution
Because there's no setup friction: testing ships with the toolchain, so you mark a function #[test] and run cargo test with nothing to install or configure. Every project tests the same way, so the habit transfers and there's no framework decision to stall on. The lower the friction, the more tests get written.
Enough motivation. The next lesson writes a real test: the #[test] attribute, the assert! family, testing for panics, and watching cargo test report green.