Files
learn-rust/lessons/0009-writing-your-own-tests.html
T

935 lines
52 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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::&lt;Result&lt;Vec&lt;Task&gt;,
TaskError&gt;&gt;()?</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) -&gt; Thermostat {
if target &lt; MIN {
panic!("target must be at least {MIN}, got {target}");
} else if target &gt; MAX {
panic!("target must be at most {MAX}, got {target}");
}
Thermostat { target }
}
pub fn target(&amp;self) -&gt; i32 {
self.target
}
// never leaves the legal range, however big `by` is
pub fn warmer(&amp;mut self, by: i32) {
self.target = capped(self.target + by);
}
pub fn is_heating(&amp;self, room: i32) -&gt; bool {
room &lt; self.target
}
// "21" -&gt; Ok, "hot" or "99" -&gt; Err
pub fn set_from(&amp;mut self, text: &amp;str) -&gt; Result&lt;(), String&gt; {
let degrees: i32 = text
.trim()
.parse()
.map_err(|_| format!("not a number: {text}"))?;
if degrees &lt; MIN || degrees &gt; 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) -&gt; 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 &lt;name&gt;</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 &gt; 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() -&gt; Result&lt;(), String&gt; {
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&lt;(), TaskError&gt;</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&lt;T, E&gt;</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&lt;T, E&gt;</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&lt;T, E&gt;</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
--&gt; 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
--&gt; src/lib.rs:48:1
|
48 | fn capped(degrees: i32) -&gt; i32 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0616]: field `target` of struct `Thermostat` is private
--&gt; 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: &amp;[String],
store: &amp;mut Store,
out: &amp;mut impl Write, // std::io::Write
) -&gt; Result&lt;(), TaskError&gt;</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&lt;u8&gt;</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(&amp;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&lt;(), TaskError&gt;</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: &amp;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&lt;u8&gt;</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(&amp;[&amp;str]) -&gt; Vec&lt;String&gt;</code>, and <code>three_tasks() -&gt; 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&lt;(), TaskError&gt;</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: &amp;[String],
store: &amp;mut Store,
out: &amp;mut impl Write,
) -&gt; Result&lt;(), TaskError&gt;</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&lt;io::Error&gt; 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, "{:&lt;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&lt;u8&gt;</code> for <code>out</code>, then
<code>String::from_utf8</code>:</p>
<pre><code>fn output(command: &amp;[&amp;str], store: &amp;mut Store) -&gt; String {
let mut out: Vec&lt;u8&gt; = Vec::new();
run(&amp;args(command), store, &amp;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&lt;&amp;str&gt;</code> borrowed from <code>&amp;self</code>, and
<code>Status::label</code> returns <code>&amp;str</code> borrowed from <code>&amp;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>-&gt; Result&lt;(), E&gt;</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>&amp;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 &amp; 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>