DEV Community

Cover image for 30 Days of Rust - Day 23
johnnylarner
johnnylarner

Posted on

30 Days of Rust - Day 23

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)