Rust unit testing test types
Understand the building blocks for automated testing in Rust

So you have finally learned Rust and are writing cool projects with it? Awesome! However, as soon as your project starts being useful and solving some actual use cases, you realize that the quality of your code is important. Not only that. The larger and more complex your project becomes, the harder it is to manually test every bit of functionality after a change.
Your first temptation might be: "Why should I test everything if my changes only affect this small part of the program?" And, while in some situations that might be true, more often than not our changes affect unexpected parts of our program.
Writing automated tests is one of the most amusing and intellectually rewarding aspects of software development. It helps you break the problem into the right bite-sized parts and let you know when you have properly solved them. Thus, for a long while, I have wanted to write a book on this topic. However, I never find the time to put the content together and I have decided to release a series of articles on Rust automated testing that contain and extend the contents that I have delivered in some of my courses and workshops.
In this first article, I want to cover the basics of unit testing in Rust; the building blocks. I will briefly explain the syntax, but I'd like to go further and get you into the mindset of writing automated tests for the win. I will expand it to some other aspects in future articles, but, at the very least, I hope this is a gentle introduction to the concepts behind testing, starting by what to test and how to test it.
Although I have some material on the tools used for testing, I will start with plain vanilla Rust, because this is all that we need to understand and write good tests. Sharpen your favorite editor –I will be using Emacs here– and your mind and, if this your first experience with automated testing, get ready to change the way you look at your code. There is no way back.
Basic Unit Testing
First things first. I'm not going to make any assumptions on your background on unit testing. Hence, the safest way to start is by describing the "building blocks" for adding tests to your code and then we will write one of each type, step by step.
There are three types of tests that we can write in a language like Rust. Those are:
- Return tests
- where we test the return of a function or a method.
- State test
- where we test the state of a type instance after interacting with it.
- Behavior test
- where we test the interactions between different type instances of our program.
Don't worry if this seems too abstract or not obvious right away. It will be clearer when we start writing the code, but you can use this taxonomy later to understand how to approach each type of test later on. So, let's begin with some actual code.
Project creation
We will start by creating a new project using cargo. We all have learned from movies that working for the bad guys can be very profitable, so we are going to devote our new project to evil plans to rule the world. I have chosen the name of the project accordingly and I hope that you like it.
cargo new detestable-me

Return Test
In a return test we work with a function or method that returns something. We use that function or method with a set of values for the arguments, most times even set the state of the instance, and check if the value produced and returned is the one that was expected.
Writing our first type
We are going to start by creating of a type, named "Supervillain" in its own Rust module, that will be placed in its own
(new) file called supervillain.rs
.
pub struct Supervillain {}
In case of doubt, you have the final version of the
supervillain.rs
file at the bottom of this article.
But, as you already know, that module won't be included in the compilation of this project unless we include it at the
beginning of main.rs
:
mod supervillain;
There is not much to test yet, so let's add a public string property for the first name of the supervillain and reconsider. Should we test that property? No! Setting and getting the value of a property is part of the functionality provided by the language. We haven't added any functionality, so there is nothing to test yet. The same happens when we add a second property for the last name. No test either.
pub first_name: String,
pub last_name: String,
Let's add some actual functionality in an implementation block and define a method to return the full name of the supervillain1. That, we can test.
impl Supervillain {
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}
Define a (sub)module for tests
The first step for having tests is to create a submodule for them and we put it in the same file that contains the
Supervillain
:
mod tests {}
But we don't want this to be part of the production code when it gets released and deployed. Rather, we would like the compiler to include this module only for the tests, so we let it know that by using the conditional compilation attribute preceding the module declaration:
#[cfg(test)]
This newly created submodule is where we are going to put our tests for the supervillain and we will need access from
within it to the public interface of the module that we are testing. We obtain access to the public methods of the
supervillain
module by importing the main code of this file into the namespace of the tests module.
use super::*;
Ok! The groundwork is done. But before we start writing our tests, let's add some tools to our toolbox.
Snippets for VS Code
If you use VS Code, I recommend you to use snippets to simplify the creation of the tests and have a scaffold that will
help you approach their implementation using a predefined structure. VS Code already has built-in snippets for test modules
(tmod
) and test functions (tfn
). I will show you how to create a snippet that you can customize to your taste and
needs.
Select Help => Show all commands
(or Shift-Command-P in macOS) and choose "Snippets: Configure user snippets" and then
"rust":

VS Code will open a file named rust.json
, containing comments that explain how to create a snippet. First, we will
add a snippet for creating the test module, as we just did manually.
{ // Already exising curly brackets
"Test modules": {
"prefix": "tmod",
"body": [
"#[cfg(test)]",
"mod tests {",
"$0",
"}"
],
"description": "Module for tests"
}
}
And then one for creating new tests. In this second snippet, we will have comments to make the structure of the test explicit with a comment for each the three parts. Some people like to refer to them by (1) given, (2) when, (3) then, but I feel more comfortable remembering the AAA: (1) arrange, (2) act, (3) assert. Feel free to change it to use the one that resonates better with you.
{ // Already exising curly brackets
"Test function with comments": {
"prefix": "tfnwc",
"body": [
"#[test]",
"fn ${1:test_name} {",
"// Arrange",
"$0",
"// Act",
"// Assert",
"}"
],
"description": "Template for tests with comments for the different parts"
}
}
Testing return: full name is first name space last name

This is where the rubber meets the road: we are going to write our first test. We will test that the method
full_name()
returns a string according to our functional requirements, i.e., the full name is the result of
concatenating the first name, a space, and the last name.
We use the snippet for the test and provide the test name. Although this might seem irrelevant, it isn't. Don't be
fooled by Shakespeare2 and use a meaningful name. We want our name to be telling about what we are testing and
what we expect. The reason for this is that when we run the tests, should any of them fail, we want to know what is
wrong with the code without having to review what that test was checking. Hence, "test1", "test_supervillain",
"test_full_name", or "full_name_is_ok" aren't good enough. In this case, I will stay with
full_name_is_first_name_space_last_name
.
If you decide to apply this rule, but still curse me for all the time wasted on figuring out good test names and the money spent on additional characters, please remember to also bless me for all the time you save by not having to re-read your tests when they don't pass. 😄
This is the skeleton of our first test:
#[test]
fn full_name_is_first_name_space_last_name() {
// Arrange
// Act
// Assert
}
The #[test]
attribute allows Rust test runner to identify this function as a test and run it when told to do so.
Now, let's write what we need in each of the stages, starting with "arrange". In the arrange part, we set up the scenario for testing. If you still remember your latest science laboratory, this where we set the controlled environment, defining the scenario that allows us to execute what we want and to know what to expect.
In this case, we want to create an instance of the supervillain, which we will call sut
. You can choose any other
name, but there is a long tradition of using that name to refer to the System Under Test. This is all that we need to
arrange for this first test.
let sut = Supervillain {
first_name: "Lex".to_string(),
last_name: "Luthor".to_string(),
};
In the "act" stage, we want to exercise the function/method that we want to test. This is the full_name()
method and
we store the return value from that function in a(n immutable) variable.
let full_name = sut.full_name();
Finally, in the assert stage, we declare our expectations. We use the assert_eq!
macro to state that the return value
should be equal to "Lex Luthor".
assert_eq!(full_name, "Lex Luthor");
I would like to highlight the importance of using a literal for the expected value rather than computing it. Even though it would be possible to use the following assertion instead of the previous one, it wouldn't be a good idea because, in case it fails, we would have a hard time figuring out if the problem took place in the method being tested or in the expression that we used to obtain the expected value.
// Don't do this
assert_eq!(full_name, format!("{} {}", sut.first_name, sut.last_name));
Our first test is ready. Let's try it. We use cargo for that.
cargo test
You can always turn it up a notch by adding a spoken message with the result.
cargo test && say "Tests passed" || say "Tests failed"
A test ain't good until you have seen it fail. For the sake of completeness, let's make it fail by removing the space
between first and last name in the format!
expression of the method and run it again.
format!("{}{}", self.first_name, self.last_name)
You should get a me
running 1 test test supervillain::tests::full_name_is_first_name_space_last_name ... FAILED failures: ---- supervillain::tests::full_name_is_first_name_space_last_name stdout ---- thread 'supervillain::tests::full_name_is_first_name_space_last_name' panicked at src/supervillain.rs:27:9: assertion `left == right` failed left: "LexLuthor" right: "Lex Luthor" note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: supervillain::tests::full_name_is_first_name_space_last_name test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--bin detestable-me`
Although the message is pretty clear in this case, we can make it more explicit by adding information to the assertion.
assert_eq!(full_name, "Lex Luthor", "Unexpected full name");
Running it again produces the following output, that contains the additional information:
running 1 test test supervillain::tests::full_name_is_first_name_space_last_name ... FAILED failures: ---- supervillain::tests::full_name_is_first_name_space_last_name stdout ---- thread 'supervillain::tests::full_name_is_first_name_space_last_name' panicked at src/supervillain.rs:28:9: assertion `left == right` failed: Unexpected full name left: "LexLuthor" right: "Lex Luthor" note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: supervillain::tests::full_name_is_first_name_space_last_name test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--bin detestable-me`
Please remember to add the space back to the
format!
expression and make the test pass again.
State Test
In a state test we work with some method that changes the state of an instance of the type. We set the instance to some known state, use the method, and check if the state of the instance has changed as expected.
Test state: setter sets first name and last name

We have a method that returns the full name of supervillain using the information contained in the fields of an instance. We can add one that reverses the process: takes a full name and sets the values of the first and the last names. We are going to start with a not very robust implementation that would not work properly unless the provided full name contains two words separated by one space. It is far from perfect, but it will do for writing the corresponding test.
pub fn set_full_name(&mut self, name: &str) {
let components = name.split(" ").collect::<Vec<_>>();
self.first_name = components[0].to_string();
self.last_name = components[1].to_string();
}
Let's write the test step by step again. We create the scaffold for the test using a meaningful name.
#[test]
fn set_full_name_sets_first_and_last_names() {
// Arrange
// Act
// Assert
}
In the arrange stage, we create an instance of Supervillain
. The same one we were using for our previous test, only
this time we make it mutable so we can change its state.
let mut sut = Supervillain {
first_name: "Lex".to_string(),
last_name: "Luthor".to_string(),
};
The act part is as simple as invoking the new method with a new supervillain name. "Darth Vader" in this case.
sut.set_full_name("Darth Vader");
And then, the assert stage must state the expected value for the two fields of the instance. While in general, it is a bad idea to make more than one assertion per test, there are exceptions to that. In this case, since both assertions point to the same part of the code, it is ok to use more than one,
assert_eq!(sut.first_name, "Darth");
assert_eq!(sut.last_name, "Vader");
If we run the tests again, we will get the two tests passing. Hooray!
Yo hablo Rust
I can hear you thinking: "but this isn't the way we do things in Rust". While it certainly works, this is ersatz. It
isn't very idiomatic. A more common way to implement the same or similar functionality would be to create a constructor
(fn new(name: &str) -> Option<Supervillain>
) or an implementation of the TryFrom
trait, but those need dealing with
optionals or error handling and I don't want to go there yet.
impl TryFrom<&str> for Supervillain {
type Error = anyhow::Error;
fn try_from(name: &str) -> Result<Self, Self::Error> {
let components = name.split(" ").collect::<Vec<_>>();
if components.len() == 2 {
Ok(Supervillain {
first_name: components[0].to_string(),
last_name: components[1].to_string(),
})
} else {
Err(anyhow!("String must contain first and last name."))
}
}
}
A middle ground option is to use an associated function without error handling or an implementation of the From trait. Let's implement the later, although that would be testing return instead of state:
impl From<&str> for Supervillain {
fn from(name: &str) -> Self {
let components = name.split(" ").collect::<Vec<_>>();
Supervillain {
first_name: components[0].to_string(),
last_name: components[1].to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_str_slice_produces_supervillain_full_with_first_and_last_name() {
// Act
let sut = Supervillain::from("Darth Vader");
// Assert
assert_eq!(sut.first_name, "Darth");
assert_eq!(sut.last_name, "Vader");
}
}
Notice that this new test doesn't have an arrange stage part. There is nothing that needs to be arranged before testing the desired method.
Behavior Test
In a behavior test we work with a method or a function that interacts with another type instance. We set the state of the instance with the method and the arguments for calling it. That includes providing the instance that it will interact with. Then, we use the method or function and check if it has interacted with the latter instance as expected.
The simplest interaction
We need to have an scenario in which our supervillain interacts with another type. Bad guys are good at attacking and interacting with weapons and supervillains do the same with megaweapons.
Let's define what a megaweapon is with a trait. It basically is anything that can be shot, so, it contains a single
method, shoot()
, that takes no parameters and returns nothing:
pub trait Megaweapon {
fn shoot(&self);
}
As for our supervillain, we add it a new method to provide them with the ability to attack. The method takes a weapon as a parameter, and shoots the weapon:
pub fn attack(&self, weapon: impl Megaweapon) {
weapon.shoot();
}
Test behavior: Supervillain attacks using weapon

What we want to test in this scenario is that when a supervillain attacks, they shoot the megaweapon. As in the previous examples, let's start with the skeleton of the test.
#[test]
fn attack_shoots_weapon() {
// Arrange
// Act
// Assert
}
The arrange part is quite straightforward: we create an instance of our system under test. The same one we have used before.
let sut = Supervillain {
first_name: "Lex".to_string(),
last_name: "Luthor".to_string(),
};
The act part isn't that obvious. Yes, we want to use the attack()
method of the sut, but that method requires
something that implements the Megaweapon
trait and we haven't defined any type that does that, yet. Does this mean
that we cannot write this test? No way! On the contrary, we can create our own megaweapon and use it to our advantage.
Don't worry. We don't need to become a lord of war for that.

Since this weapon is meant only for testing purposes, we are going to define the type inside of the tests
module and
call it WeaponDouble
3. The only important thing is that it must implement the Megaweapon
trait. We will take
care of the details later.
struct WeaponDouble {}
impl Megaweapon for WeaponDouble {
fn shoot(&self) {
todo!()
}
}
With this new "weapon" available, we can take a step forward and create an instance in the arrange part and use it in the act part for attacking.
let weapon = WeaponDouble {};
// Act
sut.attack(weapon);
We are almost there. We are only missing the assertion. But, what should we assert? Anything about the sut?
Clearly not! Let's take another look at the name of our test: attack_shoots_weapon
. It says it all. When the sut
attacks, we expect the weapon to be shot. So the assertion should be that the weapon is shot, but how do we do that?
Forget about the code for a moment. Imagine that you have your terrific supervillain in front of you and you want to ensure that when he attacks, he shoots the megaweapon. You don't want the real thing to happen. You don't want the supervillain to shoot a real megaweapon. Even less so when you are around. So you hand him a fake gun. One that registers when the trigger is pushed, but does no harm. That should be enough to know if our supervillain does what he is expected to do.
Back to code then. We add a field to the WeaponDouble
struct to keep track of when it is shot (is_shot
). For
convenience, we add an associated function that creates a WeaponDouble
setting its initial value to false. And in the
implementation of the shoot()
method, we change it to true.
struct WeaponDouble {
pub is_shot: bool,
}
impl WeaponDouble {
fn new() -> WeaponDouble {
WeaponDouble { is_shot: false }
}
}
impl Megaweapon for WeaponDouble {
fn shoot(&self) {
self.is_shot = true;
}
}
We can replace the creation of the weapon to use the associated function.
let mut weapon = WeaponDouble::new();
And finally assert that shoot()
has been called.
assert!(weapon.is_shot);
And… that would be it if we were using most other programming languages.
But Rust doesn't like this. We compile and run the tests with cargo and we get this output:
warning: variable does not need to be mutable --> src/supervillain.rs:83:13 | 83 | let mut weapon = WeaponDouble::new(); | ----^^^^^^ | | | help: remove this `mut` | = note: `#[warn(unused_mut)]` on by default error[E0382]: use of moved value: `weapon` --> src/supervillain.rs:87:17 | 83 | let mut weapon = WeaponDouble::new(); | ---------- move occurs because `weapon` has type `WeaponDouble`, which does not implement the `Copy` trait 84 | // Act 85 | sut.attack(weapon); | ------ value moved here 86 | // Assert 87 | assert!(weapon.is_shot); | ^^^^^^^^^^^^^^ value used here after move | note: consider changing this parameter type in method `attack` to borrow instead if owning the value isn't necessary --> src/supervillain.rs:21:34 | 21 | pub fn attack(&self, weapon: impl Megaweapon) { | ------ in this method ^^^^^^^^^^^^^^^ this parameter takes ownership of the value note: if `WeaponDouble` implemented `Clone`, you could clone the value --> src/supervillain.rs:90:5 | 85 | sut.attack(weapon); | ------ you could clone this value ... 90 | struct WeaponDouble { | ^^^^^^^^^^^^^^^^^^^ consider implementing `Clone` for this type error[E0594]: cannot assign to `self.is_shot`, which is behind a `&` reference --> src/supervillain.rs:100:13 | 100 | self.is_shot = true; | ^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written | help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition | 7 ~ fn shoot(&mut self); 8 | } ... 98 | impl Megaweapon for WeaponDouble { 99 ~ fn shoot(&mut self) { | Some errors have detailed explanations: E0382, E0594. For more information about an error, try `rustc --explain E0382`. warning: `detestable-me` (bin "detestable-me" test) generated 1 warning error: could not compile `detestable-me` (bin "detestable-me" test) due to 2 previous errors; 1 warning emitted
And while in most cases, the advice from the Rust compiler is between a useful magic crystal ball and a mind reader that
knows your real intentions, in this occasion the suggestion to make the reference to self mutable for fn shoot(&mut self);
is not something
we want to do. You don't want to make parameters mutable for the sake of testing. That is way beyond designing your
public interface to be testable. So much for our attempt to test behavior. Should we call it a day?
Once more with feeling
Our previous attempt presents two problems that wouldn't be an issue with many other programming languages:
- On the one hand, the
Megaweapon
trait method takes a shared reference to self, i.e., the test double. Its state cannot be changed. - On the other hand, functions/methods that consume the argument are hard to test, because the test double cannot be queried after being moved to the function/method.
The first problem can be solved using interior mutability, i.e., references with rules checked at run-time. We can use
a RefCell
and borrow a reference with mutable access to it in the shoot()
method.
struct WeaponDouble {
pub is_shot: RefCell<bool>,
}
impl WeaponDouble {
fn new() -> WeaponDouble {
WeaponDouble {
is_shot: RefCell::new(false),
}
}
}
impl Megaweapon for WeaponDouble {
fn shoot(&self) {
*self.is_shot.borrow_mut() = true;
}
}
After doing this, the reference to the weapon test double that we create doesn't have to be mutable.
let weapon = WeaponDouble::new();
The second one can be solved implementing the Drop
trait that defines what to do before the instance of our
test double is released. There we will check if it has been shot or panic otherwise. Panicking might seem a little
extreme, but this actually what an assert macro does when the assertion is not true. It is a way to signal the test
runner that this test didn't pass.
impl Drop for WeaponDouble {
fn drop(&mut self) {
if *self.is_shot.borrow() != true {
panic!("Failed to call shoot()");
}
}
}
By implementing the Drop
trait, we can get rid of the assertion in this test, because it happens automatically when
the weapon test double gets out of scope.
Use cargo test
and all the tests should pass.
Still, if you own the design of the Supervillain
interface and passing a reference instead of ownership is a valid
option for your problem at hand, it is always easier to make the API of the object testable. We can make attack()
take a shared reference to something that implements Megaweapon
.
pub fn attack(&self, weapon: &impl Megaweapon) {
weapon.shoot();
}
In this way, we can remove the implementation of the Drop
trait and use a regular assertion.
let weapon = WeaponDouble::new();
sut.attack(&weapon);
assert!(*weapon.is_shot.borrow());
Final code
This is the final version –for this article– of the supervillain
file. You can also find the repo with all the code
in this series here.
pub struct Supervillain {
pub first_name: String,
pub last_name: String,
}
pub trait Megaweapon {
fn shoot(&self);
}
impl Supervillain {
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
pub fn set_full_name(&mut self, name: &str) {
let components = name.split(" ").collect::<Vec<_>>();
self.first_name = components[0].to_string();
self.last_name = components[1].to_string();
}
pub fn attack(&self, weapon: &impl Megaweapon) {
weapon.shoot();
}
}
impl From<&str> for Supervillain {
fn from(name: &str) -> Self {
let components = name.split(" ").collect::<Vec<_>>();
Supervillain {
first_name: components[0].to_string(),
last_name: components[1].to_string(),
}
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
#[test]
fn full_name_is_first_name_space_last_name() {
// Arrange
let sut = Supervillain {
first_name: "Lex".to_string(),
last_name: "Luthor".to_string(),
};
// Act
let full_name = sut.full_name();
// Assert
assert_eq!(full_name, "Lex Luthor", "Unexpected full name");
}
#[test]
fn set_full_name_sets_first_and_last_names() {
// Arrange
let mut sut = Supervillain {
first_name: "Lex".to_string(),
last_name: "Luthor".to_string(),
};
// Act
sut.set_full_name("Darth Vader");
// Assert
assert_eq!(sut.first_name, "Darth");
assert_eq!(sut.last_name, "Vader");
}
#[test]
fn from_str_slice_produces_supervillain_full_with_first_and_last_name() {
// Act
let sut = Supervillain::from("Darth Vader");
// Assert
assert_eq!(sut.first_name, "Darth");
assert_eq!(sut.last_name, "Vader");
}
#[test]
fn attack_shoots_weapon() {
// Arrange
let sut = Supervillain {
first_name: "Lex".to_string(),
last_name: "Luthor".to_string(),
};
let weapon = WeaponDouble::new();
// Act
sut.attack(&weapon);
// Assert
assert!(*weapon.is_shot.borrow());
}
struct WeaponDouble {
pub is_shot: RefCell<bool>,
}
impl WeaponDouble {
fn new() -> WeaponDouble {
WeaponDouble {
is_shot: RefCell::new(false),
}
}
}
impl Megaweapon for WeaponDouble {
fn shoot(&self) {
*self.is_shot.borrow_mut() = true;
}
}
impl Drop for WeaponDouble {
fn drop(&mut self) {
if *self.is_shot.borrow() != true {
panic!("Failed to call shoot()");
}
}
}
}
Summary
In this article, I have explained the three types of tests that we can use with Rust and many other programming languages. I have created a piece of code from scratch and tested its functionality using unit test. On the way, I have introducing some best practices for writing tests, like its structure or how to name them. Last but not least, I have highlighted some nuances of Rust and how they affect to writing unit tests.
Stay curious. Hack your code. See you next time!
Footnotes
Admittedly, I have been lazy here and assumed that the full name is the result of putting the first name first and the last/family name later separated by a space. Not all cultures do it like that. At the very least, I know some Asian cultures do it the other way around and I was taught that Hungarians do that too. Even in my own culture as an Spaniard, it doesn't work that way because we use two last names. But this should do as an example, so please bear with me.
“What's in a name? That which we call a rose by any other name would smell just as sweet.”, William Shakespeare
I have a plenty of material about test doubles. Stay tuned!