Lesson 7.3 · 12 min
Writing tests
Chatterbox now has a dozen commands and a sentence for every mistake. Each time you change one line, any of them could quietly break, and checking them all by hand means typing every command again. A test is a promise you write down once, and the computer checks it for you every time, forever.
Adds to ChatterboxChatterbox gets its first tests.
Checking by hand does not last
Think about what you did after lesson 7.2. To be sure Chatterbox still worked, you would start it, type a name, then /say, /last two, /dance, a long message, a question, and read every answer. That takes a minute. Now imagine doing it after every change, for months. Nobody does, so sooner or later a sentence changes by accident and nobody notices.
A test is a small function that does that checking for you. It calls a piece of your program with an input you chose, and says what the answer must be. You write it once. After that, one command runs every test you have ever written, in about a second, and tells you if any promise is broken. The computer never gets bored and never skips one.
Here is the smallest useful test, on a tiny program separate from Chatterbox.
$ cargo run
HELLOWhat each line means
The program printed HELLO and nothing else. The test is in the file but the program never ran it: tests have their own command, which you meet in a moment. Read the new lines from the top.
#[test]is an attribute: a label in#[...]that tells the compiler something about the item under it. This one says "this function is a test". A test takes nothing and usually gives nothing back (it may also return aResult<(), E>, so it can use?from lesson 7.2; anErrthen counts as a failure).#[cfg(test)]says "only build what follows when testing" (cfg is short for configuration). Your finished program does not carry its tests around, so they cost it nothing.mod tests { ... }puts the tests in a named box of their own, a module. Modules get a full lesson in chapter 8; for now it is the conventional place for tests, kept apart from the real code.use super::*;reaches out of that box.supermeans "the code around this module" and*means "every name in it", so the tests can callshoutas if they were next to it.assert_eq!(left, right)is the check itself: the two values must be equal.
Here is the rule that makes all of it work. A test passes if its function finishes, and fails if it panics, the emergency stop you met with unwrap() on a None. The assert macros are simply tidy ways to panic with a useful message when something is wrong. Each test runs on its own thread, a separate line of work inside the program, so one test panicking is recorded and the rest carry on.
| You write | It panics (the test fails) when |
|---|---|
assert!(x) | x is false |
assert_eq!(a, b) | a and b differ; it prints both |
assert!(matches!(v, pattern)) | v does not fit the pattern (lesson 5.5) |
r.unwrap_err() | r was an Ok: it gives you the reason inside an Err, the mirror image of unwrap() |
The assert macros work in ordinary code too, which makes them easy to try. Guess what this program does.
Does this compile — and if it does, does it run?
- It compiles and runs
- It compiles, then panics
- Compile error E0308
Reveal the answer
It compiles, then panics at run time. The first promise holds. The second is wrong about the program: reply("") is "you said: ", which is not empty, so assert! panics and all good is never printed. That is a test doing its job: either the code or the promise is wrong, and now you know.
Running them: cargo test
Chatterbox gets its first tests in this lesson (you will see them below). In a terminal, in the project folder, the command is cargo test. This is a real run:
$ cargo test
Compiling chatterbox v0.7.0 (~/chatterbox)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.71s
Running unittests src/main.rs (target/debug/deps/chatterbox-b65eb7a24000c288)
running 7 tests
test tests::help_has_a_short_form ... ok
test tests::last_needs_a_number ... ok
test tests::plain_text_is_chat ... ok
test tests::reply_echoes_and_answers_questions ... ok
test tests::say_needs_text ... ok
test tests::reply_refuses_long_messages ... ok
test tests::unknown_commands_are_errors ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sRead it from the top. Cargo builds a special version of the program with the tests switched on (that is what #[cfg(test)] waits for), then runs it. Each test gets one line: its full name, tests:: plus the function's name, and ok. The order is not the order in the file, because the tests run at the same time on several threads and report as they finish. The last line is the verdict: seven passed, none failed.
What a failing test looks like
A test earns its keep on the day it fails. So break something on purpose: change the word said to typed inside reply, the kind of small edit anyone might make while tidying, and run the tests again.
$ cargo test
Compiling chatterbox v0.7.0 (~/chatterbox)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.24s
Running unittests src/main.rs (target/debug/deps/chatterbox-b65eb7a24000c288)
running 7 tests
test tests::help_has_a_short_form ... ok
test tests::plain_text_is_chat ... ok
test tests::last_needs_a_number ... ok
test tests::reply_refuses_long_messages ... ok
test tests::say_needs_text ... ok
test tests::unknown_commands_are_errors ... ok
test tests::reply_echoes_and_answers_questions ... FAILED
failures:
---- tests::reply_echoes_and_answers_questions stdout ----
thread 'tests::reply_echoes_and_answers_questions' (92541362) panicked at src/main.rs:387:9:
assertion `left == right` failed
left: "you typed: hi"
right: "you said: hi"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::reply_echoes_and_answers_questions
test result: FAILED. 6 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--bin chatterbox`Six still pass, and one is named with FAILED. Underneath, the report says which test, the exact line of the assert (src/main.rs:387), and both sides of the comparison: left is what the code produced, right is what the test promised. You did not have to run Chatterbox or type anything. The broken sentence was found in a fraction of a second, by a line you wrote once.
Chatterbox v0.7: its first tests
How do you choose what to test? Start with the things you would otherwise check by hand: each promise the program makes to the person typing. For Chatterbox that is how a line is understood, and what the bot says back. The seven tests sit at the bottom of main.rs, under a comment:
plain_text_is_chat: ordinary text becomesCommand::Chatholding that text. It usesmatches!with an extra condition,if t == "hello there", after the pattern: the pattern must fit and the condition must be true.help_has_a_short_form:/helpand/hboth meanHelp.say_needs_text,last_needs_a_numberandunknown_commands_are_errorspin down theCommandErrorsentences from lesson 7.2. Each callsparse, takes the reason withunwrap_err(), turns it into text withto_string()(which uses theDisplayblock) and compares it word for word.reply_echoes_and_answers_questionsandreply_refuses_long_messagescheck the three kinds of answer. The long message is made with"x".repeat(MAX_LEN + 1), text repeated that many times: one byte over the limit, written using the limit itself, so the test still means the same thing ifMAX_LENever changes.
Nothing else in the program changed, and it behaves exactly as before: cargo run never sees the tests.
Sam first second /last 2 /last two /last /last 99 /quit
Chatterbox v0.7 What should I call you? Hi Sam! I'm Chatterbox. Ask me anything. bot: you said: first bot: you said: second you: second bot: you said: second bot: 'two' is not a count. Try /last 3 bot: last what? Try /last 3 you: first bot: you said: first you: second bot: you said: second bot: bye, Sam! messages so far: 2
Download this version (zip)Unzip, then cargo run inside the folder.
Chatterbox might one day notice when you are shouting. Write is_shouting: it is true when the text has at least one letter and none of its letters are lowercase, so "STOP!" is shouting but "Stop" and "123!" are not. Then finish the two tests at the bottom, which say todo!() for now: each should check the case its comment describes. The hidden tests check your function; your own tests have to pass too.
Chatterbox might one day notice when you are shouting. Write is_shouting: it is true when the text has at least one letter and none of its letters are lowercase, so "STOP!" is shouting but "Stop" and "123!" are not. Then finish the two tests at the bottom, which say todo!() for now: each should check the case its comment describes. The hidden tests check your function; your own tests have to pass too.
Hidden tests (7): check_empty_text_is_not_shouting check_no_letters_is_not_shouting check_shouting_with_spaces_and_marks check_one_small_letter_is_enough_to_stop_it capitals_are_shouting no_letters_is_not_shouting mixed_case_is_not_shouting
This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.
Say it out loud, then reveal
What makes a test pass or fail?Reveal
assert! and assert_eq! panic when their promise is broken.Why are tests put inside <code>#[cfg(test)] mod tests</code>?Reveal
cargo test. The finished program does not carry them, and use super::*; lets them call the code around them.In a failed <code>assert_eq!</code>, what are left and right?Reveal
What does <code>unwrap_err()</code> give you?Reveal
Err. If the value was an Ok it panics, which in a test means the test fails.That is lesson 7.3. Tick it off so the course remembers where you are.
Go deeper
Questions people ask
How do I write a test in Rust?
Write a function with #[test] above it, call the code you want to check, and use assert_eq! or assert! to say what the answer must be. Put tests in a module marked #[cfg(test)], usually called tests.
How do I run tests in Rust?
Run cargo test in the project folder. It builds the program with the tests switched on, runs every test and prints ok or FAILED for each, then a summary line.
What does cfg(test) mean in Rust?
It tells the compiler to build the code that follows only when testing. Your normal program, built with cargo run or cargo build, leaves the tests out.
What does use super::* do inside a tests module?
It brings every name from the surrounding code into the tests module, so the tests can call your functions directly by name.
Why do my Rust tests run in a different order each time?
cargo test runs tests at the same time on several threads, so they finish and report in whatever order they happen to complete. Each test must not depend on another one running first.