935 lines
52 KiB
HTML
935 lines
52 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>0009 — Writing your own tests</title>
|
||
<link rel="stylesheet" href="../assets/style.css" />
|
||
</head>
|
||
<body>
|
||
|
||
<h1>Writing your own tests</h1>
|
||
<p class="subtitle">Lesson 0009 · after <a href="0008-iterators-and-hashmap.html">0008</a> · reading, then a 45-minute drill graded by planted bugs · ~55 minutes</p>
|
||
|
||
<div class="callout">
|
||
Every code block, every compiler message, and every terminal session on this page was produced by running it
|
||
today. Nothing is written from memory. Where a <code>cargo test</code> block is quoted, the
|
||
<code>Compiling</code> / <code>Finished</code> lines and the empty <code>Doc-tests</code> section are cut and
|
||
nothing else. The demo domain is a thermostat, defined in full two sections down — your project is a task CLI,
|
||
so nothing here pastes in. Translating is the work.
|
||
</div>
|
||
|
||
<h2>Where 0008 left you, and the bug that 46 tests could not see</h2>
|
||
|
||
<p>The library half of 0008 landed cleanly. All 46 tests pass, <code>tally</code> is a real generic function
|
||
written from its signature, <code>load</code> is one <code>collect::<Result<Vec<Task>,
|
||
TaskError>>()?</code>, <code>remove_completed</code> uses <code>Vec::retain</code>, and
|
||
<code>count_by_priority</code> is a one-line delegate. Iterators and <code>HashMap</code> are produced, not
|
||
recognised.</p>
|
||
|
||
<p>Three of the drill's checks still failed, and the interesting thing is where they failed. Here is your
|
||
<code>stats</code> and <code>clear</code>, run today against your own crate:</p>
|
||
|
||
<pre><code>$ run stats
|
||
high 1
|
||
low 1
|
||
medium 1
|
||
$ run clear
|
||
$ run list
|
||
2 [todo] call bank (medium)
|
||
3 [todo] water plants (low)</code></pre>
|
||
|
||
<p>The priorities print in the wrong order — <code>high</code>, <code>low</code>, <code>medium</code>, because
|
||
<code>main.rs</code> loops over <code>[Priority::High, Low, Medium]</code> — and <code>clear</code> prints
|
||
nothing at all, throwing away the <code>usize</code> that <code>remove_completed</code> went to the trouble of
|
||
returning. Both are real defects a user would notice in the first minute. Both sat behind 46 green tests.</p>
|
||
|
||
<p>That is not bad luck, and it is not because you were careless. It is structural: <strong>every one of those
|
||
46 tests lives in <code>tests/</code>, every one of them talks to the library, and <code>run</code> lives in
|
||
<code>src/main.rs</code>, where no test in <code>tests/</code> can reach it.</strong> The untested thing is the
|
||
undone thing — this is the third lesson in a row where the one requirement no test could see is the one
|
||
requirement that was not met. Today you close that loop from both ends: you learn to write tests, and you move
|
||
the code that was unreachable into a place where a test can reach it.</p>
|
||
|
||
<h2>The demo domain, in full</h2>
|
||
|
||
<p>Every example on this page runs against one small crate, so it is worth reading the whole thing once before
|
||
the examples start. It is a thermostat that refuses illegal targets. Three things to notice as you read: the
|
||
field <code>target</code> is private, the helper <code>capped</code> has no <code>pub</code>, and
|
||
<code>new</code> panics while <code>set_from</code> returns a <code>Result</code> — the page needs both to show
|
||
you both ways of testing failure:</p>
|
||
|
||
<pre><code>// /tmp/heating/src/lib.rs — created with `cargo new --lib heating`
|
||
pub const MIN: i32 = 5;
|
||
pub const MAX: i32 = 30;
|
||
|
||
#[derive(Debug, PartialEq)]
|
||
pub struct Thermostat {
|
||
target: i32, // degrees celsius, always inside MIN..=MAX
|
||
}
|
||
|
||
impl Thermostat {
|
||
// panics if the target is outside the legal range
|
||
pub fn new(target: i32) -> Thermostat {
|
||
if target < MIN {
|
||
panic!("target must be at least {MIN}, got {target}");
|
||
} else if target > MAX {
|
||
panic!("target must be at most {MAX}, got {target}");
|
||
}
|
||
Thermostat { target }
|
||
}
|
||
|
||
pub fn target(&self) -> i32 {
|
||
self.target
|
||
}
|
||
|
||
// never leaves the legal range, however big `by` is
|
||
pub fn warmer(&mut self, by: i32) {
|
||
self.target = capped(self.target + by);
|
||
}
|
||
|
||
pub fn is_heating(&self, room: i32) -> bool {
|
||
room < self.target
|
||
}
|
||
|
||
// "21" -> Ok, "hot" or "99" -> Err
|
||
pub fn set_from(&mut self, text: &str) -> Result<(), String> {
|
||
let degrees: i32 = text
|
||
.trim()
|
||
.parse()
|
||
.map_err(|_| format!("not a number: {text}"))?;
|
||
if degrees < MIN || degrees > MAX {
|
||
return Err(format!("out of range: {degrees}"));
|
||
}
|
||
self.target = degrees;
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
// private helper: no `pub`, so only this file can call it
|
||
fn capped(degrees: i32) -> i32 {
|
||
degrees.clamp(MIN, MAX)
|
||
}</code></pre>
|
||
|
||
<p>One naming convention, because it is the only way to read the examples without guessing:
|
||
<code>Thermostat</code> (capitalised) is the type, <code>t</code> is always a value of it, and
|
||
<code>room</code> is always the current room temperature rather than the target. The mapping onto your crate is
|
||
loose on purpose — this is a different program, not a template. What transfers is the shape of a test, not its
|
||
subject.</p>
|
||
|
||
<h2>Part 1 — A test is a function that fails by panicking</h2>
|
||
|
||
<p>The whole mechanism is one attribute. Put <code>#[test]</code> on a function that takes no arguments and
|
||
returns nothing, and <code>cargo test</code> builds a second binary out of your crate, runs every such
|
||
function, and reports on each one. The book's definition is worth reading slowly, because the second half of it
|
||
is the part people never internalise:</p>
|
||
|
||
<blockquote>
|
||
<p>Tests fail when something in the test function panics. Each test is run in a new thread, and when the main
|
||
thread sees that a test thread has died, the test is marked as failed.</p>
|
||
</blockquote>
|
||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 — How to
|
||
write tests</a></p>
|
||
|
||
<p>So there is no assertion framework here and no special test runtime. <em>Panicking is the failure
|
||
protocol.</em> Every assertion macro you are about to meet is a thin wrapper that panics when its condition
|
||
does not hold, which is why <code>.unwrap()</code> in a test body is not a code smell the way it is in
|
||
<code>main</code> — an <code>unwrap</code> that fires is a test that fails, with the message you wanted
|
||
anyway.</p>
|
||
|
||
<p>Here are two tests against the thermostat, and the output they produce. Read the output as carefully as the
|
||
code, because the failure format is the thing you will actually spend your time reading:</p>
|
||
|
||
<pre><code>#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn a_new_thermostat_keeps_its_target() {
|
||
let t = Thermostat::new(20);
|
||
assert_eq!(t.target(), 20);
|
||
}
|
||
|
||
#[test]
|
||
fn warmer_never_passes_the_maximum() {
|
||
let mut t = Thermostat::new(28);
|
||
t.warmer(10);
|
||
assert_eq!(t.target(), MAX);
|
||
}
|
||
}</code></pre>
|
||
<pre><code> Running unittests src/lib.rs (target/debug/deps/heating-795830f7b1de3879)
|
||
|
||
running 2 tests
|
||
test tests::a_new_thermostat_keeps_its_target ... ok
|
||
test tests::warmer_never_passes_the_maximum ... ok
|
||
|
||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</code></pre>
|
||
|
||
<p>You have read that summary line 46 times without needing it. Now it is yours, so take the five fields
|
||
apart once: <code>passed</code> and <code>failed</code> are self-explanatory; <code>ignored</code> counts tests
|
||
marked <code>#[ignore]</code>, which Part 5 covers; <code>measured</code> is for nightly-only benchmarks and
|
||
will always be <code>0</code> for you; and <code>filtered out</code> counts tests that exist but did not run
|
||
because you passed a name filter. Note also that the test name is <code>tests::a_new_thermostat…</code> —
|
||
<strong>the module path is part of the test's name</strong>, which is what makes filtering by module possible
|
||
later.</p>
|
||
|
||
<p>Now the same run with a bug planted in <code>capped</code>, which drops the upper bound
|
||
(<code>degrees.clamp(MIN, MAX)</code> becomes <code>degrees.max(MIN)</code>):</p>
|
||
|
||
<pre><code>running 2 tests
|
||
test tests::a_new_thermostat_keeps_its_target ... ok
|
||
test tests::warmer_never_passes_the_maximum ... FAILED
|
||
|
||
failures:
|
||
|
||
---- tests::warmer_never_passes_the_maximum stdout ----
|
||
|
||
thread 'tests::warmer_never_passes_the_maximum' (192610) panicked at src/lib.rs:66:9:
|
||
assertion `left == right` failed
|
||
left: 38
|
||
right: 30
|
||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||
|
||
|
||
failures:
|
||
tests::warmer_never_passes_the_maximum
|
||
|
||
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||
|
||
error: test failed, to rerun pass `--lib`</code></pre>
|
||
|
||
<p>Three sections, and each answers a different question. The per-test lines say <em>which</em> tests ran. The
|
||
<code>failures:</code> block with the stdout capture says <em>why</em> each failure happened, and it is the
|
||
only place the panic message appears. The short <code>failures:</code> list at the end is just names, so that
|
||
with forty tests and six failures you can copy one name and re-run it alone. The final
|
||
<code>error: test failed, to rerun pass <code>--lib</code></code> is cargo telling you which target to narrow
|
||
to — <code>--lib</code> for unit tests, <code>--test <name></code> for one integration file.</p>
|
||
|
||
<h2>Part 2 — Three macros, and what each failure tells you</h2>
|
||
|
||
<p>You only need three, and the choice between them is entirely about what you want printed when the test
|
||
fails. That is the whole design question: a passing test prints nothing interesting, so a macro earns its keep
|
||
only by how much it tells you on the day it goes red.</p>
|
||
|
||
<table>
|
||
<tr><th>Macro</th><th>Fails when</th><th>Prints</th></tr>
|
||
<tr><td><code>assert!(cond)</code></td><td><code>cond</code> is false</td><td>the source text of <code>cond</code></td></tr>
|
||
<tr><td><code>assert_eq!(a, b)</code></td><td><code>a != b</code></td><td>both values, as <code>left</code> and <code>right</code></td></tr>
|
||
<tr><td><code>assert_ne!(a, b)</code></td><td><code>a == b</code></td><td>both values, the same way</td></tr>
|
||
</table>
|
||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 — Testing
|
||
equality with <code>assert_eq!</code> and <code>assert_ne!</code></a></p>
|
||
|
||
<p>Prefer <code>assert_eq!</code> whenever you have an expected value to name, because a bare
|
||
<code>assert!</code> throws away the numbers. Compare these two failures of the same bug — the room comparison
|
||
in <code>is_heating</code> flipped to <code>room > self.target</code>. First, <code>assert!</code> on its
|
||
own:</p>
|
||
|
||
<pre><code>#[test]
|
||
fn a_cold_room_heats() {
|
||
let t = Thermostat::new(20);
|
||
assert!(t.is_heating(18));
|
||
}</code></pre>
|
||
<pre><code>thread 'tests::a_cold_room_heats' (196474) panicked at src/lib.rs:59:9:
|
||
assertion failed: t.is_heating(18)</code></pre>
|
||
|
||
<p>That tells you the expression was false, which you could have guessed from the test's name. When the
|
||
condition is a <code>bool</code> and there is nothing to compare, add the message yourself — every argument
|
||
after the condition is handed to <code>format!</code>, so you can print whatever would have helped:</p>
|
||
|
||
<pre><code>#[test]
|
||
fn a_cold_room_heats() {
|
||
let t = Thermostat::new(20);
|
||
assert!(
|
||
t.is_heating(18),
|
||
"a room at 18 must heat towards {}",
|
||
t.target()
|
||
);
|
||
}</code></pre>
|
||
<pre><code>thread 'tests::a_cold_room_heats' (196562) panicked at src/lib.rs:59:9:
|
||
a room at 18 must heat towards 20</code></pre>
|
||
|
||
<p>Your shipped tests use this in a place worth copying. In <code>tests/persist.rs</code> the loop over corrupt
|
||
lines ends with <code>"line {:?} should be reported as a bad line", bad</code>, because the assertion runs six
|
||
times and the failure would otherwise not say <em>which</em> line broke it. That is the rule: <strong>if an
|
||
assertion runs inside a loop, it needs a message naming the case</strong>, or a red test sends you back to
|
||
guessing.</p>
|
||
|
||
<p>One requirement comes attached to <code>assert_eq!</code>, and you have already satisfied it without
|
||
knowing. To print the two values, the macro needs <code>Debug</code>; to compare them, it needs
|
||
<code>PartialEq</code>:</p>
|
||
|
||
<blockquote>
|
||
<p>When the assertions fail, these macros print their arguments using debug formatting, which means the values
|
||
being compared must implement the <code>PartialEq</code> and <code>Debug</code> traits.</p>
|
||
</blockquote>
|
||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1</a></p>
|
||
|
||
<p>This is why <code>#[derive(Debug, PartialEq)]</code> sits on <code>Task</code>, <code>Status</code>,
|
||
<code>Priority</code>, <code>Command</code>, and <code>Store</code> — not decoration, a testing requirement.
|
||
And it is why <code>TaskError</code> has that hand-written <code>impl PartialEq</code> from 0007:
|
||
<code>io::Error</code> does not implement it, so the derive was impossible and you compared
|
||
<code>kind()</code> instead. Every one of those impls exists so that <code>assert_eq!</code> can print
|
||
something useful. Today you are finally the one calling it.</p>
|
||
|
||
<h2>Part 3 — Two ways to test a failure</h2>
|
||
|
||
<p>Code that works is the easy half. The interesting tests are the ones that pin down what happens when the
|
||
input is wrong, and Rust gives you two tools because your code has two ways to fail: it panics, or it returns
|
||
an <code>Err</code>.</p>
|
||
|
||
<p>For a panic, annotate the test with <code>#[should_panic]</code>, and the test passes if and only if the
|
||
body panics. Always give it <code>expected</code>, a substring of the panic message, or the test will happily
|
||
pass on a panic that came from somewhere else entirely:</p>
|
||
|
||
<pre><code>#[test]
|
||
#[should_panic(expected = "at most 30")]
|
||
fn refuses_a_high_target() {
|
||
Thermostat::new(99);
|
||
}</code></pre>
|
||
<pre><code>running 2 tests
|
||
test tests::a_text_target_is_read_or_reported ... ok
|
||
test tests::refuses_a_high_target - should panic ... ok</code></pre>
|
||
|
||
<p>Notice the <code>- should panic</code> marker in the result line: the runner tells you the test's polarity
|
||
is inverted, which matters when you are reading someone else's suite. And here is the same test against a
|
||
<code>new</code> whose upper-bound branch was given the lower bound's message by mistake — the code still
|
||
panics on 99, but says the wrong thing:</p>
|
||
|
||
<pre><code>thread 'tests::refuses_a_high_target' (197030) panicked at src/lib.rs:15:13:
|
||
target must be at least 5, got 99
|
||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||
note: panic did not contain expected string
|
||
panic message: "target must be at least 5, got 99"
|
||
expected substring: "at most 30"</code></pre>
|
||
|
||
<p>Without <code>expected</code> that run would have been green, and the test would have been worthless: it
|
||
would have proved only that <em>something</em> went wrong. The <code>expected</code> substring is what turns
|
||
"it panicked" into "it panicked for the reason I meant".</p>
|
||
|
||
<p>For an <code>Err</code>, the tool is different and better suited to your crate: a test may return
|
||
<code>Result</code>, which lets you use <code>?</code> in its body. The test passes on <code>Ok</code> and
|
||
fails on <code>Err</code>:</p>
|
||
|
||
<pre><code>#[test]
|
||
fn a_text_target_is_read_or_reported() -> Result<(), String> {
|
||
let mut t = Thermostat::new(20);
|
||
t.set_from(" 21 ")?; // an Err here fails the test
|
||
assert_eq!(t.target(), 21);
|
||
assert!(t.set_from("hot").is_err());
|
||
Ok(())
|
||
}</code></pre>
|
||
|
||
<p>Two rules come with that shape, and the second one is the trap. First, the return type has to be a
|
||
<code>Result</code> whose error implements <code>Debug</code> — <code>Result<(), TaskError></code>
|
||
qualifies, since <code>TaskError</code> derives <code>Debug</code>. Second, from the book:</p>
|
||
|
||
<blockquote>
|
||
<p>You can't use the <code>#[should_panic]</code> annotation on tests that use <code>Result<T, E></code>.
|
||
To assert that an operation returns an <code>Err</code> variant, <em>don't</em> use the question mark operator
|
||
on the <code>Result<T, E></code> value. Instead, use <code>assert!(value.is_err())</code>.</p>
|
||
</blockquote>
|
||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 — Using
|
||
<code>Result<T, E></code> in tests</a></p>
|
||
|
||
<p>Read those two together and the division of labour is clear. Use <code>?</code> for the steps that are
|
||
merely <em>setup</em> — the save, the load, the completion that has to work before the interesting assertion
|
||
can run — and use an explicit <code>assert_eq!(…unwrap_err(), …)</code> or
|
||
<code>assert!(…is_err())</code> for the failure you are actually testing. Your shipped
|
||
<code>tests/errors.rs</code> does the second half already; the drill has you write the first.</p>
|
||
|
||
<p>Which means, honestly, that <code>#[should_panic]</code> has almost no place in your crate — and that is a
|
||
result, not a gap. Since 0006 your code returns <code>TaskError</code> instead of panicking, so there is no
|
||
panic left to pin. Learn the attribute because interview questions and other people's crates use it; reach for
|
||
the <code>Result</code> form in your own.</p>
|
||
|
||
<h2>Part 4 — Where tests live, and what each kind can see</h2>
|
||
|
||
<p>Rust has exactly two homes for tests, and the choice is not stylistic — it decides what your test is allowed
|
||
to touch:</p>
|
||
|
||
<blockquote>
|
||
<p><em>Unit tests</em> are small and more focused, testing one module in isolation at a time, and can test
|
||
private interfaces. <em>Integration tests</em> are entirely external to your library and use your code in the
|
||
same way any other external code would, using only the public interface and potentially exercising multiple
|
||
modules per test.</p>
|
||
</blockquote>
|
||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 — Test
|
||
organization</a></p>
|
||
|
||
<p>A unit test lives in the same file as the code it tests, at the bottom, in a module with two attributes'
|
||
worth of ceremony:</p>
|
||
|
||
<pre><code>#[cfg(test)] // compile this only for `cargo test`
|
||
mod tests {
|
||
use super::*; // pull the whole parent module into scope
|
||
|
||
#[test]
|
||
fn the_private_cap_holds_both_ends() {
|
||
assert_eq!(capped(99), MAX); // private fn, reachable
|
||
assert_eq!(capped(-40), MIN);
|
||
assert_eq!(capped(21), 21);
|
||
}
|
||
}</code></pre>
|
||
|
||
<p>Both lines earn their place. <code>#[cfg(test)]</code> means the module is not compiled into
|
||
<code>cargo build</code> output at all, so tests cost nothing in the shipped binary. <code>use super::*</code>
|
||
is what gives the test its reach: the <code>tests</code> module is an ordinary child module, and a child may
|
||
see its parent's private items — which is the entire reason unit tests can test private functions. No
|
||
annotation grants that privilege; the module tree does, exactly as chapter 7 described it.</p>
|
||
|
||
<p>An integration test lives in <code>tests/</code>, and gets a very different deal. Each file there is
|
||
compiled as its own separate crate that <code>use</code>s yours from outside, so it sees precisely what a
|
||
stranger on crates.io would see. Ask for anything private and the compiler says so — this is a real
|
||
<code>cargo test</code> run of a <code>tests/outside.rs</code> that tries both:</p>
|
||
|
||
<pre><code>error[E0603]: function `capped` is private
|
||
--> tests/outside.rs:7:25
|
||
|
|
||
7 | assert_eq!(heating::capped(99), 30); // so is the helper
|
||
| ^^^^^^ private function
|
||
|
|
||
note: the function `capped` is defined here
|
||
--> src/lib.rs:48:1
|
||
|
|
||
48 | fn capped(degrees: i32) -> i32 {
|
||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
|
||
error[E0616]: field `target` of struct `Thermostat` is private
|
||
--> tests/outside.rs:6:18
|
||
|
|
||
6 | assert_eq!(t.target, 20); // the field is private
|
||
| ^^^^^^ private field
|
||
|
|
||
help: a method `target` also exists, call it with parentheses
|
||
|
|
||
6 | assert_eq!(t.target(), 20); // the field is private
|
||
| ++</code></pre>
|
||
|
||
<p>You have met <code>E0616</code> before from the other side. In 0003 you made <code>Store.tasks</code>
|
||
private and added the <code>tasks()</code> accessor, and every one of my 46 tests goes through that accessor
|
||
because it has no choice. So the trade is now concrete: put a test in <code>src/</code> and it can reach
|
||
inside; put it in <code>tests/</code> and it is forced to use the API you actually ship, which means it also
|
||
notices when you break that API. Write both kinds, for different reasons — the file-local ones to pin down
|
||
awkward internals, the external ones to pin down the contract.</p>
|
||
|
||
<p>Sharing a helper between two integration files has one gotcha, and it is worth spending a paragraph on
|
||
because you will hit it in the drill. Since every file in <code>tests/</code> is its own crate, a
|
||
<code>tests/common.rs</code> full of helpers is compiled as a <em>test crate of its own</em> and shows up in
|
||
the output as a pointless <code>running 0 tests</code> section. The fix is the older module-file spelling:</p>
|
||
|
||
<pre><code>tests/
|
||
├── common/
|
||
│ └── mod.rs ← helpers live here; not treated as a test crate
|
||
├── cli.rs ← `mod common;` then `common::three_tasks()`
|
||
└── mine.rs ← same, its own crate, its own copy</code></pre>
|
||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 —
|
||
Submodules in integration tests</a>: “Files in subdirectories of the <em>tests</em> directory don't get compiled
|
||
as separate crates or have sections in the test output.”</p>
|
||
|
||
<p>And now the rule this whole lesson turns on. It is one paragraph in the book, and it explains your
|
||
<code>stats</code> bug exactly:</p>
|
||
|
||
<blockquote>
|
||
<p>If our project is a binary crate that only contains a <em>src/main.rs</em> file and doesn't have a
|
||
<em>src/lib.rs</em> file, we can't create integration tests in the <em>tests</em> directory and bring functions
|
||
defined in the <em>src/main.rs</em> file into scope with a <code>use</code> statement. … This is one of the
|
||
reasons Rust projects that provide a binary have a straightforward <em>src/main.rs</em> file that calls logic
|
||
that lives in the <em>src/lib.rs</em> file.</p>
|
||
</blockquote>
|
||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 —
|
||
Integration tests for binary crates</a></p>
|
||
|
||
<p>Your crate has both files, which is why the tests can see <code>Store</code> at all. But your
|
||
<code>run</code> function — the one that decides the order of the <code>stats</code> lines and whether
|
||
<code>clear</code> says anything — is defined in <code>main.rs</code>, on the wrong side of that wall. No test
|
||
can import it. The book's advice is the fix: <code>main.rs</code> should be small enough that it needs no
|
||
test, and everything else belongs in the library. Step 3 of the drill moves <code>run</code> across.</p>
|
||
|
||
<p>Moving it is not enough on its own, though, and the second half is the more useful trick. A <code>run</code>
|
||
that calls <code>println!</code> writes to the process's stdout, which a test cannot read. So instead of
|
||
printing, take the destination as a parameter:</p>
|
||
|
||
<pre><code>pub fn run(
|
||
args: &[String],
|
||
store: &mut Store,
|
||
out: &mut impl Write, // std::io::Write
|
||
) -> Result<(), TaskError></code></pre>
|
||
|
||
<p><code>main</code> hands it <code>io::stdout().lock()</code> and behaves exactly as before. A test hands it
|
||
a <code>Vec<u8></code>, which implements <code>Write</code>, and then asserts on the bytes. That is the
|
||
whole technique: <strong>a function that returns or writes its output can be tested; a function that prints its
|
||
output cannot.</strong> It costs one parameter, and it is the single most reusable idea in this lesson —
|
||
the same move makes an HTTP handler testable without a server, and it is the answer to the interview question
|
||
“how would you test that?”</p>
|
||
|
||
<h2>Part 5 — Running them: the flags worth knowing</h2>
|
||
|
||
<p>Everything so far assumed a bare <code>cargo test</code>. Four flags cover the rest of daily use, and the
|
||
first thing to know is where the separator goes: arguments before <code>--</code> are read by cargo,
|
||
arguments after it are read by the test binary cargo just built.</p>
|
||
|
||
<table>
|
||
<tr><th>Command</th><th>What it does</th></tr>
|
||
<tr><td><code>cargo test warmer</code></td><td>runs tests whose full name contains <code>warmer</code></td></tr>
|
||
<tr><td><code>cargo test --lib</code></td><td>only the unit tests inside <code>src/</code></td></tr>
|
||
<tr><td><code>cargo test --test cli</code></td><td>only <code>tests/cli.rs</code></td></tr>
|
||
<tr><td><code>cargo test -- --show-output</code></td><td>also print stdout from tests that passed</td></tr>
|
||
<tr><td><code>cargo test -- --ignored</code></td><td>only the tests marked <code>#[ignore]</code></td></tr>
|
||
<tr><td><code>cargo test -- --test-threads=1</code></td><td>no parallelism</td></tr>
|
||
</table>
|
||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-02-running-tests.html">11.2 —
|
||
Controlling how tests are run</a></p>
|
||
|
||
<p>Filtering matches on the whole test name, module path included, which is the payoff of that
|
||
<code>tests::</code> prefix from Part 1. A real run, with three of four tests filtered out:</p>
|
||
|
||
<pre><code>$ cargo test warmer
|
||
Running unittests src/lib.rs (target/debug/deps/heating-795830f7b1de3879)
|
||
|
||
running 1 test
|
||
test tests::warmer_never_passes_the_maximum ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s</code></pre>
|
||
|
||
<p>Output capture is the behaviour that surprises people: a <code>println!</code> in a passing test is
|
||
swallowed, and only reappears if the test fails. When you want to see it anyway, ask:</p>
|
||
|
||
<pre><code>$ cargo test -- --show-output
|
||
running 1 test
|
||
test tests::warmer_never_passes_the_maximum ... ok
|
||
|
||
successes:
|
||
|
||
---- tests::warmer_never_passes_the_maximum stdout ----
|
||
target ended at 30
|
||
|
||
|
||
successes:
|
||
tests::warmer_never_passes_the_maximum
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s</code></pre>
|
||
|
||
<p><code>#[ignore]</code> is for the test you want to keep but not run every time — the slow one, the one that
|
||
needs a network. It takes a reason string, which the runner prints, and the ignored tests are still one command
|
||
away:</p>
|
||
|
||
<pre><code>#[test]
|
||
#[ignore = "slow: walks the whole range"]
|
||
fn every_legal_target_round_trips() {
|
||
for degrees in MIN..=MAX {
|
||
let mut t = Thermostat::new(MIN);
|
||
t.set_from(&degrees.to_string()).expect("legal target");
|
||
assert_eq!(t.target(), degrees);
|
||
}
|
||
}</code></pre>
|
||
<pre><code>$ cargo test
|
||
running 4 tests
|
||
test tests::every_legal_target_round_trips ... ignored, slow: walks the whole range
|
||
test tests::refuses_a_high_target - should panic ... ok
|
||
test tests::the_private_cap_holds_both_ends ... ok
|
||
test tests::warmer_never_passes_the_maximum ... ok
|
||
|
||
test result: ok. 3 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||
|
||
$ cargo test -- --ignored
|
||
running 1 test
|
||
test tests::every_legal_target_round_trips ... ok
|
||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s</code></pre>
|
||
|
||
<p>The last flag comes with the one hard constraint of the whole chapter. Tests run <em>in parallel</em>, on
|
||
threads, by default:</p>
|
||
|
||
<blockquote>
|
||
<p>Because the tests are running at the same time, you must make sure your tests don't depend on each other or
|
||
on any shared state, including a shared environment, such as the current working directory or environment
|
||
variables.</p>
|
||
</blockquote>
|
||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch11-02-running-tests.html">11.2 — Running
|
||
tests in parallel or consecutively</a></p>
|
||
|
||
<p>Your suite obeys this already, and now you can see why it was written that way. Every file-touching test
|
||
calls <code>temp_path()</code>, which mixes the process id with an atomic counter to produce a path no other
|
||
test will ever use. That is the first solution the book offers — one file per test. <code>--test-threads=1</code>
|
||
is the second, and it is a worse one: it is slower, and it hides the coupling instead of removing it. Reach for
|
||
it to diagnose a flaky suite, not to fix one. The same reasoning is why no test of yours may set
|
||
<code>TASKS_FILE</code>: environment variables are per-process, so a test that sets one is reaching into every
|
||
other test running at that moment.</p>
|
||
|
||
<h2>Part 6 — A test that cannot fail is not a test</h2>
|
||
|
||
<p>Green tests are not evidence. Forty-six of them were green while <code>stats</code> printed its lines in the
|
||
wrong order, and no amount of staring at the count would have told you. The only honest question about a test
|
||
suite is: <em>which bugs would it catch?</em></p>
|
||
|
||
<p>There is a mechanical way to ask it, called mutation testing. Plant a deliberate bug in a copy of the code,
|
||
run the suite, and see whether it goes red. A bug the suite notices is <em>killed</em>. A bug it sleeps
|
||
through <em>survives</em>, and every survivor is a precise, undeniable description of a missing test. This
|
||
lesson ships six of them in <a href="0009-mutants.sh">0009-mutants.sh</a>: it copies your crate to a temp
|
||
directory, applies one <code>sed</code> substitution, and runs your tests. Your own files are never
|
||
touched.</p>
|
||
|
||
<p>Here is that script run against your crate exactly as it stands right now, before the drill:</p>
|
||
|
||
<pre><code>$ bash ~/learn-rust/lessons/0009-mutants.sh ~/learn-rust/tasks
|
||
crate: /home/tan/learn-rust/tasks
|
||
SKIP stats-order src/cli.rs does not exist yet
|
||
SKIP stats-zero src/cli.rs does not exist yet
|
||
SKIP clear-count src/cli.rs does not exist yet
|
||
SURVIVED list-format src/task.rs
|
||
SURVIVED status-parse src/task.rs
|
||
SURVIVED command-case src/command.rs
|
||
|
||
0 killed, 3 survived, 3 skipped</code></pre>
|
||
|
||
<p>Read the six lines as a to-do list, because that is what they are. The three <code>SKIP</code>s are the
|
||
mutations that live in <code>src/cli.rs</code> — the file you have not written yet, which is where
|
||
<code>run</code> is going. The three <code>SURVIVED</code>s are real bugs your 46 tests cannot see today:
|
||
<code>Display for Task</code> could stop printing the priority, <code>Status::parse</code> could stop
|
||
understanding <code>in-progress</code>, and <code>Command::parse</code> could stop accepting
|
||
<code>ADD</code> in capitals, and every test would still pass. The drill's finishing condition is
|
||
<code>6 killed, 0 survived</code>.</p>
|
||
|
||
<p>One caveat, so you calibrate the tool correctly rather than worshipping it: a suite that kills every mutant
|
||
is not a proven-correct suite, because my six mutants are not every possible bug. Mutation testing gives you a
|
||
floor, not a ceiling. It is still the sharpest feedback available on a suite you just wrote, and it is far
|
||
better than counting tests.</p>
|
||
|
||
<h2>Check yourself before the drill</h2>
|
||
|
||
<p>Six questions before you touch the keyboard. Answer each one out loud, in full sentences, before you reveal
|
||
or click. Two of them revisit 0005–0008 rather than today's material, which is deliberate — retrieval of old
|
||
work is what keeps it.</p>
|
||
|
||
<div class="q" data-type="mcq" data-topic="Tests">
|
||
<p class="topic">Tests</p>
|
||
<p class="prompt">What actually makes a <code>#[test]</code> function fail?</p>
|
||
<div class="options">
|
||
<button class="opt" data-correct="false">It returns <code>false</code></button>
|
||
<button class="opt" data-correct="true">Its thread panics</button>
|
||
<button class="opt" data-correct="false">An <code>assert!</code> returns an error</button>
|
||
<button class="opt" data-correct="false">It prints to stderr</button>
|
||
</div>
|
||
<div class="explain hidden">A test fails when something in it panics; each test runs on its own thread, and the harness marks the test failed when that thread dies. Every assertion macro is a wrapper that panics when its condition does not hold — which is why <code>unwrap</code> in a test body is fine, and why a test may also fail by returning <code>Err</code> from a <code>Result</code>-returning test.</div>
|
||
</div>
|
||
|
||
<div class="q" data-type="recall" data-topic="Tests">
|
||
<p class="topic">Tests</p>
|
||
<p class="prompt">A unit test and an integration test: where does each file live, and what can each one reach?</p>
|
||
<button class="reveal-btn">Show answer</button>
|
||
<div class="answer hidden">A unit test lives at the bottom of the source file it tests, in <code>#[cfg(test)] mod tests</code> with <code>use super::*</code>; because it is a child module it can reach its parent's private items — private functions, private fields. An integration test lives in <code>tests/</code>, is compiled as its own separate crate, and can only reach the public API, exactly as an outside user would. Reaching for a private item from there gives <code>E0603</code> (private function) or <code>E0616</code> (private field).</div>
|
||
<div class="grade hidden">
|
||
<button data-grade="hit">Got it</button>
|
||
<button data-grade="miss">Missed it</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="q" data-type="mcq" data-topic="Tests">
|
||
<p class="topic">Tests</p>
|
||
<p class="prompt">You write a test that returns <code>Result<(), TaskError></code> and want to assert a call fails. What is the correct move?</p>
|
||
<div class="options">
|
||
<button class="opt" data-correct="true"><code>assert!(call().is_err())</code></button>
|
||
<button class="opt" data-correct="false">Add <code>#[should_panic]</code></button>
|
||
<button class="opt" data-correct="false"><code>call()?</code> and expect red</button>
|
||
<button class="opt" data-correct="false">Return <code>Err</code> from the test</button>
|
||
</div>
|
||
<div class="explain hidden"><code>#[should_panic]</code> is not allowed on a test that returns <code>Result</code>, and <code>?</code> on the failing call would turn the expected failure into a test failure. Use <code>?</code> only for setup steps that must succeed, and assert the interesting failure explicitly with <code>assert!(…is_err())</code> or <code>assert_eq!(…unwrap_err(), TaskError::NotFound(9))</code>.</div>
|
||
</div>
|
||
|
||
<div class="q" data-type="recall" data-topic="Traits">
|
||
<p class="topic">Traits</p>
|
||
<p class="prompt">Why can <code>assert_eq!</code> compare and print two <code>Task</code> values, and why did <code>TaskError</code> need a hand-written <code>PartialEq</code>?</p>
|
||
<button class="reveal-btn">Show answer</button>
|
||
<div class="answer hidden"><code>assert_eq!</code> compares with <code>==</code>, so it needs <code>PartialEq</code>, and prints the two values with debug formatting on failure, so it needs <code>Debug</code>. <code>Task</code> derives both. <code>TaskError</code> cannot derive <code>PartialEq</code> because it holds an <code>io::Error</code>, which does not implement it — so 0007 wrote the impl by hand and compared <code>kind()</code> for that variant. The derives are a testing requirement, not decoration.</div>
|
||
<div class="grade hidden">
|
||
<button data-grade="hit">Got it</button>
|
||
<button data-grade="miss">Missed it</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="q" data-type="mcq" data-topic="Collections">
|
||
<p class="topic">Collections</p>
|
||
<p class="prompt">Why can a test not assert on <code>count_by_priority()</code> by iterating the map and printing as it goes?</p>
|
||
<div class="options">
|
||
<button class="opt" data-correct="true"><code>HashMap</code> order is arbitrary</button>
|
||
<button class="opt" data-correct="false">Iterating a map borrows it mutably</button>
|
||
<button class="opt" data-correct="false">The map is not <code>PartialEq</code></button>
|
||
<button class="opt" data-correct="false">Tests cannot iterate a <code>HashMap</code></button>
|
||
</div>
|
||
<div class="explain hidden">Iteration order over a <code>HashMap</code> is arbitrary and may differ between runs, so any assertion on the sequence is a coin toss. Assert on the map as a whole (it is <code>PartialEq</code>), or ask for specific keys, or — as <code>stats</code> does — impose your own order by looping over <code>[High, Medium, Low]</code> and asking the map for each. That last one is exactly the line the drill makes testable.</div>
|
||
</div>
|
||
|
||
<div class="q" data-type="recall" data-topic="Modules">
|
||
<p class="topic">Modules</p>
|
||
<p class="prompt">Your <code>run</code> lives in <code>src/main.rs</code>. Why can no file in <code>tests/</code> import it, and what are the two changes that make its output testable?</p>
|
||
<button class="reveal-btn">Show answer</button>
|
||
<div class="answer hidden">Only library crates expose items for other crates to <code>use</code>; a binary crate is meant to be run, so nothing in <code>main.rs</code> is importable from <code>tests/</code> — that is why <code>main.rs</code> should stay thin and the logic should live in the library. Two changes: move <code>run</code> into the library (<code>src/cli.rs</code>, declared in <code>lib.rs</code>), and give it an <code>out: &mut impl Write</code> parameter instead of calling <code>println!</code>, so <code>main</code> can pass <code>io::stdout().lock()</code> while a test passes a <code>Vec<u8></code> and asserts on the bytes.</div>
|
||
<div class="grade hidden">
|
||
<button data-grade="hit">Got it</button>
|
||
<button data-grade="miss">Missed it</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
|
||
<button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
|
||
|
||
<h2>The drill — 45 minutes, your own crate</h2>
|
||
|
||
<p>Type it, do not paste it. The thermostat above is a different program. Keep the
|
||
<a href="../reference/rust-syntax.html#tests">tests reference</a> open — looking syntax up is free.</p>
|
||
|
||
<pre><code>cd ~/learn-rust/tasks
|
||
cargo test # 46 pass, as they did yesterday
|
||
bash ../lessons/0009-mutants.sh . # 0 killed, 3 survived, 3 skipped</code></pre>
|
||
|
||
<p>Those two lines are the starting position. Every test you write today is yours — I am shipping no new spec
|
||
file, because the skill being built is writing the assertions rather than satisfying them. The finishing line
|
||
is the mutant report reading <code>6 killed, 0 survived, 0 skipped</code>, and all 46 existing tests still
|
||
green.</p>
|
||
|
||
<h3>Step 0 — the last 0008 leftover, one minute</h3>
|
||
|
||
<p>Delete the commented-out <code>for</code> loop still sitting inside <code>Store::find</code>. The iterator
|
||
version is one line above it and git remembers the old one.</p>
|
||
|
||
<p><strong>Check:</strong> <code>grep -c "for " src/store.rs</code> prints <code>0</code>.</p>
|
||
|
||
<h3>Step 1 — your first <code>#[test]</code>, in <code>src/task.rs</code></h3>
|
||
|
||
<p>Add a <code>#[cfg(test)] mod tests</code> at the bottom of <code>src/task.rs</code> with three tests, and
|
||
one more at the bottom of <code>src/stats.rs</code>. All four target behaviour that none of my 46 tests
|
||
touches — that is why they are worth your keystrokes rather than being duplicates.</p>
|
||
|
||
<p>The first pins the <em>line format on disk</em>: build a <code>Task</code> with a known id, title, priority
|
||
and status, assert that <code>to_line()</code> produces exactly the string you expect, and assert that parsing
|
||
that string back gives the task you started with. Both directions in one test, because a round trip that only
|
||
goes one way proves nothing about the other.</p>
|
||
|
||
<p>The second pins <em>every</em> <code>Status</code> label, not just the two the CLI uses. Loop over the three
|
||
variants, and for each one assert that <code>Status::parse(status.label())</code> gives that variant back. Your
|
||
<code>in-progress</code> arm is currently unreachable from the CLI and therefore completely untested — the loop
|
||
covers it without you writing three near-identical tests. Give the assertion a failure message naming the
|
||
label, per Part 2, or a red run will not say which variant broke.</p>
|
||
|
||
<p>The third pins <em>what the user reads</em>: the <code>Display</code> impl you wrote in 0005. Assert the
|
||
exact line for a fresh task, then set its status to <code>Done</code> and assert the line again. Nothing in the
|
||
suite has ever checked this string.</p>
|
||
|
||
<p>The fourth, in <code>src/stats.rs</code>, calls <code>tally</code> with a key that is <em>owned</em> rather
|
||
than <code>Copy</code> — a closure returning <code>String</code> — and asserts both a count and the map's
|
||
length. Your <code>count_by_priority</code> only ever hands <code>tally</code> a <code>Copy</code> key, so the
|
||
generic function has never been exercised with anything else.</p>
|
||
|
||
<p><strong>Check:</strong> <code>cargo test --lib</code> → 4 passed. Note the names in the output:
|
||
<code>task::tests::…</code> and <code>stats::tests::…</code>.</p>
|
||
|
||
<details>
|
||
<summary>Forgotten what the test module looks like?</summary>
|
||
<p><code>#[cfg(test)]</code> then <code>mod tests {</code> then <code>use super::*;</code> — Part 4 has the
|
||
whole shape. Without <code>use super::*</code> you get <code>E0433: failed to resolve</code> on the first type
|
||
name, because the child module starts with an empty scope.</p>
|
||
</details>
|
||
|
||
<h3>Step 2 — shared helpers, and tests that return <code>Result</code></h3>
|
||
|
||
<p>Create <code>tests/common/mod.rs</code> — the directory spelling from Part 4, not
|
||
<code>tests/common.rs</code> — holding three helpers you will use from two files: <code>temp_path()</code>
|
||
(copy the one from the top of <code>tests/collections.rs</code>; a helper worth sharing is a helper worth
|
||
moving), <code>args(&[&str]) -> Vec<String></code>, and <code>three_tasks() -> Store</code>
|
||
which returns a store holding one task per priority with task 1 already completed. Put
|
||
<code>#![allow(dead_code)]</code> at the top of the file: each test crate uses only some of the helpers, and
|
||
without it the unused ones warn.</p>
|
||
|
||
<p>Then write <code>tests/mine.rs</code> with three tests, each returning
|
||
<code>Result<(), TaskError></code> so the setup steps can use <code>?</code>:</p>
|
||
|
||
<p><strong>Completing a task that is already done is not an error.</strong> Complete task 1 a second time and
|
||
assert it is still <code>Done</code>. Your <code>complete</code> takes this path today; the test decides that
|
||
the behaviour is deliberate rather than accidental, which is what a test is for.</p>
|
||
|
||
<p><strong>A reload sees exactly what was saved.</strong> Take <code>three_tasks()</code>, clear the completed
|
||
one, save to a <code>temp_path()</code>, load it back, and assert the loaded tasks equal the ones in memory.
|
||
The shipped suite tests save-then-load, but never after a removal.</p>
|
||
|
||
<p><strong>Saving a smaller store shortens the file.</strong> Save three tasks, remove the completed one, save
|
||
again to the same path, then read the file with <code>fs::read_to_string</code> and assert it has two lines. If
|
||
<code>save</code> ever stops truncating, this is the only test that will notice — and a save that appends
|
||
instead of replacing is a data-loss bug, not a cosmetic one.</p>
|
||
|
||
<p><strong>Check:</strong> <code>cargo test --test mine</code> → 3 passed, and a full <code>cargo test</code>
|
||
shows <em>no</em> <code>Running tests/common</code> section. If you see one, you named the file
|
||
<code>tests/common.rs</code>.</p>
|
||
|
||
<h3>Step 3 — move <code>run</code> into the library</h3>
|
||
|
||
<p>This is the structural step, and the point of it is Part 4's rule: nothing in <code>main.rs</code> can be
|
||
tested, so almost nothing should live there.</p>
|
||
|
||
<p>Create <code>src/cli.rs</code>, declare it in <code>lib.rs</code>, and move <code>run</code> into it with
|
||
this signature:</p>
|
||
|
||
<pre><code>pub fn run(
|
||
args: &[String],
|
||
store: &mut Store,
|
||
out: &mut impl Write,
|
||
) -> Result<(), TaskError></code></pre>
|
||
|
||
<p>Replace every <code>println!(..)</code> in the body with <code>writeln!(out, ..)?</code>. The
|
||
<code>?</code> is doing real work there: <code>writeln!</code> returns <code>io::Result</code>, and your
|
||
<code>From<io::Error> for TaskError</code> from 0008's step 0 converts it — the second time that impl has
|
||
paid for itself. Then <code>main</code> becomes: build the path, load the store, collect the args, take
|
||
<code>io::stdout().lock()</code>, call <code>run</code>, save, and <code>fail</code> on either error.</p>
|
||
|
||
<p>While the code is open, fix the two defects from the top of this page. <code>stats</code> loops over
|
||
<code>[Priority::High, Priority::Medium, Priority::Low]</code> — fully qualified, in that order — and prints
|
||
each with <code>writeln!(out, "{:<6} {}", p.label(), n)?</code>, so the counts line up in a column.
|
||
<code>clear</code> keeps the <code>usize</code> that <code>remove_completed</code> returns and prints
|
||
<code>cleared N completed</code>. Those exact formats are what the tests in step 4 assert, and they are the
|
||
output the 0008 session captured.</p>
|
||
|
||
<p><strong>Check:</strong> <code>grep -c "println!" src/cli.rs</code> prints <code>0</code>, and the CLI still
|
||
behaves — from a scratch directory, with
|
||
<code>run(){ TASKS_FILE=t.txt cargo run -q --manifest-path ~/learn-rust/tasks/Cargo.toml -- "$@"; }</code>:</p>
|
||
|
||
<pre><code>$ run add "buy milk" high ; run add "call bank" ; run add "water plants" low
|
||
$ run done 1
|
||
$ run stats
|
||
high 1
|
||
medium 1
|
||
low 1
|
||
$ run clear
|
||
cleared 1 completed
|
||
$ run done 9 ; echo $?
|
||
error: no task with id 9
|
||
1</code></pre>
|
||
|
||
<h3>Step 4 — the tests that catch what 46 could not</h3>
|
||
|
||
<p>Write <code>tests/cli.rs</code>. Start with a helper that runs one command against a store and gives back
|
||
exactly what it printed — a <code>Vec<u8></code> for <code>out</code>, then
|
||
<code>String::from_utf8</code>:</p>
|
||
|
||
<pre><code>fn output(command: &[&str], store: &mut Store) -> String {
|
||
let mut out: Vec<u8> = Vec::new();
|
||
run(&args(command), store, &mut out).expect("command must succeed");
|
||
String::from_utf8(out).expect("output must be utf-8")
|
||
}</code></pre>
|
||
|
||
<p>Then six tests, each asserting on the whole printed string with <code>assert_eq!</code> rather than
|
||
searching it for a substring — an exact assertion is what kills the mutants, and the escaped
|
||
<code>\n</code>s are part of the contract:</p>
|
||
|
||
<ul>
|
||
<li><code>stats</code> on a store with one task per priority prints high, then medium, then low.</li>
|
||
<li><code>stats</code> on a store with only a medium task prints <code>0</code> for the other two, on their own
|
||
lines, rather than omitting them.</li>
|
||
<li><code>clear</code> reports how many it deleted, and reports <code>0</code> the second time.</li>
|
||
<li><code>list</code> prints one line per task in insertion order.</li>
|
||
<li>A command that fails prints <strong>nothing at all</strong> — assert the <code>Err</code> is
|
||
<code>TaskError::NotFound(9)</code> and that <code>out</code> is still empty. Errors are <code>main</code>'s
|
||
job, on stderr.</li>
|
||
<li>The command word is case-insensitive: <code>ADD</code> and <code>List</code> work, because
|
||
<code>Command::parse</code> lowercases it. Untested until now, and one of the surviving mutants.</li>
|
||
</ul>
|
||
|
||
<p><strong>Check:</strong> <code>cargo test --test cli</code> → 6 passed. Full <code>cargo test</code> → 4 + 6 +
|
||
14 + 7 + 3 + 8 + 17 = <strong>59 passed</strong>, of which 13 are yours.</p>
|
||
|
||
<h3>Step 5 — hunt the mutants</h3>
|
||
|
||
<p>Run the script against your crate. Every one of the six should now be reported <code>killed</code>:</p>
|
||
|
||
<pre><code>$ bash ../lessons/0009-mutants.sh .
|
||
killed stats-order src/cli.rs
|
||
killed stats-zero src/cli.rs
|
||
killed clear-count src/cli.rs
|
||
killed list-format src/task.rs
|
||
killed status-parse src/task.rs
|
||
killed command-case src/command.rs
|
||
|
||
6 killed, 0 survived, 0 skipped</code></pre>
|
||
|
||
<p>If one survives, do not adjust the script — read the mutation it names in the source of the script, work out
|
||
which of your tests <em>should</em> have caught it, and fix that test. A survivor is never wrong: it is a bug
|
||
that your suite genuinely cannot see. If one says <code>SKIP</code>, the pattern is not in your source, which
|
||
usually means you spelled that line differently; the script prints the file so you can compare.</p>
|
||
|
||
<h3>Then stop</h3>
|
||
|
||
<p>Not today: doc tests (<code>///</code> examples that run — chapter 14), <code>#[bench]</code>,
|
||
<code>assert_cmd</code> and <code>predicates</code> for testing the binary as a subprocess,
|
||
<code>proptest</code> for generated inputs, and <code>cargo-mutants</code>, which is the real version of this
|
||
lesson's script. Each is a small step from here, and none is on the path to the next gap.</p>
|
||
|
||
<h2>What this closed</h2>
|
||
|
||
<p>Chapter 11 moves to <em>produced</em> on the <a href="../reference/book-coverage.html">coverage map</a>, and
|
||
with it the last core gap in chapters 1–11. You have now written unit tests, integration tests, shared helpers,
|
||
<code>Result</code>-returning tests, and an assertion on a command's exact output — plus the refactor that made
|
||
the last one possible, which is the part an interviewer will actually probe.</p>
|
||
|
||
<p>What is left before the job-ready floor is short, and it is no longer about the book's core:</p>
|
||
|
||
<ol>
|
||
<li><strong>ch 10.3 — lifetimes</strong>, as reading practice. You have now written two without noticing:
|
||
<code>titles_with</code> returns <code>Vec<&str></code> borrowed from <code>&self</code>, and
|
||
<code>Status::label</code> returns <code>&str</code> borrowed from <code>&self</code>. Elision filled in
|
||
both annotations for you, and reading the explicit form is a two-lesson job at most.</li>
|
||
<li><strong><code>serde</code></strong>, which replaces your <code>to_line</code>/<code>FromStr</code> pair
|
||
with two derives — worth doing <em>after</em> writing them by hand, which you now have.</li>
|
||
<li>Then <code>axum</code>, where the traits from 0005–0008 and the testing from today start paying rent
|
||
together: a handler is just a function you can call from a test.</li>
|
||
</ol>
|
||
|
||
<h2>Take it outside</h2>
|
||
|
||
<p>Here is a question with genuine disagreement behind it, which makes it a good one to ask people rather than
|
||
docs. Your <code>output</code> helper asserts on the exact bytes a command prints, which means a wording change
|
||
to <code>cleared N completed</code> breaks a test even though nothing is broken for the user. Some engineers
|
||
call that a feature — the output <em>is</em> the contract, and changing it should be deliberate. Others call it
|
||
a brittle test that will be deleted the first time it is inconvenient, and would assert only that the count
|
||
appears somewhere in the line. Ask on <a href="https://users.rust-lang.org">users.rust-lang.org</a> where they
|
||
draw that line for CLI output, and what they do differently for output a machine parses versus output a human
|
||
reads. The answers will teach you more about test design than any chapter, because it is a taste question and
|
||
the book cannot have taste for you.</p>
|
||
|
||
<h2>The five sentences worth keeping</h2>
|
||
|
||
<ol>
|
||
<li>A test fails when its thread panics; every assertion macro is a wrapper that panics, so
|
||
<code>unwrap</code> in a test is a legitimate assertion.</li>
|
||
<li>Unit tests live beside the code in <code>#[cfg(test)] mod tests</code> and can see private items;
|
||
integration tests live in <code>tests/</code>, are separate crates, and see only the public API.</li>
|
||
<li><code>#[should_panic(expected = "…")]</code> tests a panic, a <code>-> Result<(), E></code> test
|
||
lets you <code>?</code> the setup, and the two cannot be combined.</li>
|
||
<li>Nothing in <code>src/main.rs</code> is testable, so <code>main</code> stays thin and everything else moves
|
||
to the library — and a function that writes to <code>&mut impl Write</code> is testable where one that
|
||
calls <code>println!</code> is not.</li>
|
||
<li>Tests run in parallel and share nothing safely, so give every file-touching test its own path; and judge a
|
||
suite by the bugs it kills, never by the number of tests it contains.</li>
|
||
</ol>
|
||
|
||
<footer>
|
||
<p><strong>Primary source:</strong> The Rust Book
|
||
<a href="https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html">11.1 — How to Write Tests</a>, then
|
||
<a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 — Test Organization</a>,
|
||
with <a href="https://doc.rust-lang.org/stable/book/ch11-02-running-tests.html">11.2 — Controlling How Tests
|
||
Are Run</a> as reference rather than reading. After the drill, skim
|
||
<a href="https://doc.rust-lang.org/rustc/tests/index.html">the Tests chapter of the rustc book</a> for the flags
|
||
the test binary accepts.</p>
|
||
<p>Previous: <a href="0008-iterators-and-hashmap.html">0008 — Iterators, HashMap, generics</a> ·
|
||
<a href="0007-files-and-fromstr.html">0007 — Files, io::Error, FromStr</a> ·
|
||
<a href="0006-your-own-error-type.html">0006 — Your own error type</a><br />
|
||
Reference: <a href="../reference/rust-syntax.html#tests">Tests</a> ·
|
||
<a href="../reference/rust-syntax.html#iterators">Iterators</a> ·
|
||
<a href="../reference/rust-syntax.html#traits">Traits & generics</a> ·
|
||
<a href="../reference/book-coverage.html">Coverage map</a></p>
|
||
<p><strong>Ask me things.</strong> Bring the compiler output verbatim. Step 3 is the uncomfortable one: it moves
|
||
working code for no reason a user can see, and the payoff only arrives in step 4. If a paragraph did not land,
|
||
name it; that is my fault to fix, and cheaper to fix now than mid-drill.</p>
|
||
</footer>
|
||
|
||
<script src="../assets/quiz.js"></script>
|
||
</body>
|
||
</html>
|