Good evening everyone,
Apologies for the flaky posts recently. It's difficult to pull myself away from the warm summer days. That said, today's topic is something that lies close to my heart: testing!
Yesterday's questions answered
No questions to answer
Today's open questions
No open questions
What we already know
This is the first introduction to testing in this blog series. We already covered some basic stuff in this article:
- Store your unit tests in the same file as your features
- Dedicate a tests folder for integration tests
- Unit tests should be declared as a
mod
, wrapped by a#[cfg(test)]
decorator- Each test should have a
#[test]
decorator- Use any of the assertion macros for testing outputs
- Use the
#[should_panic(msg)]
decorator to test error handling- Run
cargo test
to run your suite
Choose your macro!
Rust has a few useful macros for helping developers easily validate the outputs of tests.
assert!
: Best used to evaluate boolean return values
assert_eq!
: Use this to compare two values
assert_neq!
: Most suited to when you know an answer can be non-deterministic but should explicitly not be a particular value.
Debugging macros
One useful feature of Rust's equality macros are the optional debug arguments. You can provide a format string along with arguments and these will be printed when that assertion fails:
assert_eq!(1 + 1, 4, "{} + {} must equal {}", 1, 1, 4);
This is particularly useful if the test requires additional context due to complexity.
One thing to note is that Rust encourages users to use left
and right
as aliases for values being evaluated and are not considered equivalent to expected
and actual
. The semantic concept of expected
and actual
is just ignored in Rust.
Catching the right panic
The #[should_panic]
decorator can be provided an argument expected
. This uses substring matching to evaluate whether a test panicked for the correct reason:
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
This test will only go green if the panic message contains the substring we specify.
Leveraging the ?
operator for tricky tests
We've already learned about how the ?
can be used to reduce the burden of handling Result
types in Rust. This also holds true for tests we right. The code we write will often return errors at multiple stages, so by declaring the return type of a test as a Result
, we can use the ?
operator on the code we call.
Separating your tests
Rust is the first language I've learned that asks developers to include unit test code in the source files in which the code being tested is located. Proximity to source code can make writing tests easier (especially if you only have a small screen 🐒).
The Rust compiler ignores this code when the project is built. It achieves this through the #[cfg(test)]
decorator that you use to wrap your unit test mod
.
Writing integration tests
Integration tests follow a more typical style. Your project should house the tests in the tests
folder. Any .rs
files you put here will be treated as integration tests, designed to test your code's public interfaces in a realistic fashion.
Test setup should be also be stored in this folder, though in a subdirectory. One example would be tests/common/mod.rs
. Here you could declare public functions that can be imported into your integration test files. By using the old school mod.rs
path, you can indicate to the compiler to not treat the files as tests.
Master your test runs
We use cargo
to run tests in Rust:
cargo run test
That's the most basic way to run all your tests. If you provide an argument after test
, this will be used to filter all test names. This is similar to the -k
flag in pytest.
Here are some other useful commands:
cargo test -- --show-output # Display stdout for passing tests
cargo test -- --test-threads=1 # Single-threaded runs for better reliability
cargo test -- --ignored # Run only tests with #[ignore] decorator
cargo test -- --include-ignored # Run all tests
Top comments (0)