Tests and where to from here
This part of the course is extra material and will not contain exercises, only materials and links ฅ( ິ•ᴥ• )ິฅ✧
Automated testing
Writing automated tests is an important part of software development as they help ensure that your code is correct, and that it stays correct as you make changes over time. As a result, you can refactor your code with confidence, and you can feel great knowing that your code works as intended. The Rust compiler catches many errors in code too, but it only ensures that the code is valid Rust code. It does not in any way ensure the valid Rust code works correctly for your intended purpose.
An excellent introduction to writing, running and organizing tests in Rust can be found in the Rust Book. There is little point in explaining the same thing here too in similar detail, but we'll still go over the basics briefly. Here we'll also look at a couple of things not mentioned in the book, which are doctests, testing binary application output and code coverage. We won't cover how to control how the tests are run (for this, check Rust Book 11.2 or simply run cargo test -h
).
If you're looking for only a brief overview through examples, Rust By Example is probably enough for that purpose.
The Rust package manager Cargo supports three types of tests:
- Unit tests: These tests are designed to exercise individual components or functions in isolation, ensuring that each part of your code works as intended. They are typically small, fast, and can be run frequently during development.
- Integration tests: These tests verify that multiple components work together correctly. They are usually placed in a separate directory, and focus on how different parts of your codebase interact with each other.
- Documentation tests: Rust allows you to write tests within your documentation, ensuring that your code examples in the documentation are accurate and up-to-date. These tests are run when you generate your project's documentation using cargo doc.
Unit tests
We saw a bit of unit testing in the part 10 where we initialised a package with cargo new --lib
that generates an example lib.rs
file with a function and a unit test.
Running the test can be done with cargo test
or cargo t
for short. This will run all tests in the package and the output for the test should look like the following.
The #[test]
attribute is used to mark a function as a test. Adding the attributes means that it is only evaluated when running cargo test
command. Likewise, the #[cfg(test)]
attribute is used to mark a module as a test module — the code within the test module will only be evaluated with cargo test
and not when the package is built. The assert_eq!
macro panics if its two first arguments are not equal, thus failing the test on nonequal arguments. Similarly, we can use assert!
macro to assert a boolean expression evaluating to true
.
Both assert_eq!
and assert!
macros allow additional arguments to be passed, which comprise a message format string and its arguments that will be printed if the assertion fails. This is useful for providing more information about the failure.
The use super::*;
line is used to import all parent items into the test module, which is usually what we want when writing tests inside a test module.
The test in the library example module mod tests
is an example of a unit test. Unit tests within the tested module also allow you to test private items of a module, which aren't accessible outside the module — this is the only way to test the private parts of your Rust code.
Note that many widely used languages such as Java or JavaScript do not allow testing private non-exported items, and instead only allow testing public items (although there is a JavaScript library that hacks its way around this to make it possible to unit test private variables). This is because often the public items are the only ones considered to require testing. Regardless, it can still be useful to be able to unit test a private helper function just to make sure it functions properly without making it public, and Rust allows that with unit tests (C++ does too so it's not like Rust is a special case in this).
When we only need to test the public parts of the code (which is the typical case), it's typically better to organize the tests into files separate from the tested code. When running cargo test
, Cargo looks for tests inside a special directory called tests
. Like modules marked with #[cfg(test)]
, the module inside the tests
directory are evaluated.
However, the Rust Book advocates that the tests
directory should be used for integration tests and any unit tests should be placed next to the tested source code: "You’ll put unit tests in the src directory in each file with the code that they’re testing. The convention is to create a module named tests in each file to contain the test functions and to annotate the module with cfg(test)
." Now, this may cause a chill through spine if you think of how big the source code files will become when the file would not just contain code, but in addition loads of unit tests for comprehensive testing.
Before breaking the Rust convention and placing unit tests inside the tests
directory, a worthwhile option is to split the tests inside src into separate files.
src
├── lib.rs
├── car.rs
└── car
└── tests.rs
The tests inside car/tests.rs
can test the private items of car.rs
regardless of being in a separate file since the tests are still in a submodule of car.rs
.
Returning Results instead of panicking
Test functions have to return a type that implements the std::process::Termination
trait (the main
function has this same restriction). Practically, this means that the returns either a unit ()
or a Result
. If the test function returns a Result
type, the test will pass if the Result
is Ok
, and fail if the Result
is Err
. Returning a result is useful when testing code that may ail, but we want to continue testing or setting up the test code includes potentially panicking code. We'll get to use the convenient ?
operator to propagate the errors and avoid verbose matching.
The following example contains a function reads that the last line of a file and returns it in a Result
. The test fails if the function returns incorrect data, or if the test can't create and clean up a temporary file for testing the function.
Testing for panicking
When testing code that is expected to panic, we can use the #[should_panic]
attribute to assert that the code panics. The test will pass if the code panics, and fail if the code does not panic. We can also add an expected
parameter to the attribute (#[should_panic(expected = "message")]
attribute to assert that the code panics with a specific message.
Integration tests
Integration tests, that is, tests that are primarily meant to test connection of flow between different components or application programming interfaces (APIs) are placed inside a directory named tests
. Our private car example is a library, so we may want to test every public function in it as an integration test — after all, anyone who calls the public functions expects them to work properly.
Integration tests, like unit tests, are defined with the attribute #[test]
. The modules inside tests
are not built unless running the tests, so there is no need to include the #[cfg(test)]
attribute unlike inside src
. The difference is that integration tests are placed in a separate file, tests/car.rs
, and are not in the same module as the tested code. This is because integration tests are meant to test the public interface of the library, not the implementation details.
.
├── src
├── lib.rs
└── car.rs
└── tests
└── car.rs
Testing binary crates
Binary crates cannot be tested in the same way as library crates, because the items in binary crates cannot to be imported by other crates. Instead, we can test the binary crate by running it with the assert_cmd
crate. This crate allows us to run the binary crate as a subprocess and assert that it exits successfully and prints the expected output.
For more examples and command options, see the assert_cmd
crate documentation.
Documentation tests
The third way to write tests in Rust is documentation tests. These are defined within documentation comments (three slashes ///
) inside Markdown code blocks (text inside triple backticks ```).
Documentation tests are run with the cargo test
command like the other types of tests and fail on code that panics. Although documentation tests are defined next to the code that is tested, the tested code must be public and imported with use
statements like with integration tests (use crate::
won't work).
Documentation tests also support attributes such as the should_panic
attribute, but they do not support the expected
parameter.
You can find more information about documentation tests in The rustdoc book, for example how to hide portions of the example (such as helper code for the tests) and how to use the ?
operator in doctests.
Test coverage
Rust compiler supports test coverage, although not as simply as running a single Cargo command. Instruction for how to produce test coverage with the help of json5format
crate can be found in the Rust compiler documentation.
A more comprehensive and easier-to-use coverage suite is provided by the Tarpaulin. It can be installed with
cargo install cargo-tarpaulin
Then, to run the tests and produce coverage report for a package, run
cargo tarpaulin
The coverage for the example library created with cargo new --lib <packagege-name>
should look like
... INFO cargo_tarpaulin::report: Coverage Results:
|| Uncovered Lines:
|| Tested/Total Lines:
|| src/lib.rs: 2/2
||
100.00% coverage, 2/2 lines covered
Where to from here?
This course has covered the basics of Rust and some of the more advanced features. Hopefully, you have learned enough to be able continue to learn more about Rust on your own. In addition, we hope you have gained some kind understand when Rust would be a good fit for a project and be able to start writing your own programs in Rust.
Understanding which language is a good fit for a project naturally requires taking into account other factors than just the language itself. For example, the availability of libraries and tools for the language make a major difference in how easy it is to get started with a project and how much time it can take.
Rust, while being a relatively young language, has already a broad spectrum of libraries and tools available for different kinds of projects, including embedded, web, game and desktop application development, data science and embedding Rust into projects in other languages (e.g. Python, R, Node.js). Many of these are already production ready and used in real world projects.
There are also plenty of learning resources available online, from blogs and video series to workshops and courses that help you continue your journey with Rust. A comprehensive and actively updated list of tools, libraries and resources for Rust can be found on the Awesome Rust repository.
It also worthwhile to learn or boost up your skills in other languages too to build up a wider skill set. Often single projects can require or benefit from using multiple languages. For example Tauri, an increasingly popular framework for building desktop applications builds on the idea of using HTML/CSS/JavaScript for user interface components while using Rust for the application logic and other backend processes. This makes it possible to benefit from both the multitude of existing (and familiar to many) battle-tested web frontend frameworks and the speed of Rust.
The next planned courses in this course series are an introduction course to programming languages and their history, and a Kotlin (https://kotlinlang.org, https://en.wikipedia.org/wiki/Kotlin_(programming_language)) course.
Additional resources
These are all general Rust language related resources. For resources on a specific area, e.g. web frameworks or gaming, check out the Awesome Rust repository.
Getting in touch with other Rustaceans
A good way to learn Rust better is to actively participate in discussion by asking questions and answering them, or just checking out what other Rustaceans are talking about. Links to the official Rust community forums and chat platforms can be found on their website. Other channels, such as Rust subreddit and IRC channels are listed on rustaceans.org.
From beginner onwards
All of these are available online and completely free
- The Rust Programming Language - The official Rust book
- The Rust Programming Language with Quizzes - The official Rust book, but with embedded quizzes and possibility to embed comments to the materials
- Rust By Example is a set of runnable examples that explain various Rust concepts.
- Rust Cookbook - A collection of simple examples that demonstrate various Rust concepts and standard library features.
- Rustlings is a set of small exercises to get you used to reading and writing Rust code.
Intermediate to advanced
- The Rustonomicon - A book about unsafe Rust (online and free)
- Rust in Action - A book for people who already know how to program and want to learn Rust.
- Rust for Rustaceans - A book for people who already know Rust and want to understand it further.
- Programming Rust