Masai Mahapa

30 days of Rust - Day Eleven - Testing our code

30 days of rust - day Eleven

"I'm not a great programmer; I'm just a good programmer with great habits." - Kent Beck

Rust does a lot to help us write reliable software with its amazing type and borrower checkers (read my post on ownership). However, bugs also come in the form of errors in implementing the business logic.

For example, if a function named muplity_by_3 actually adds the number 3 instead of multiplying the input. This is where testing our code can help uncover such problems.

Day 11 - Testing

There are two types of testing in Rust (and many other programming languages), these are namely Unit Tests and Integration Tests.

Basically Unit tests focus on single functions and Integration tests look at the bigger picture, how our other applications interact with ours. For this post, I shall focus on unit tests.

Let's build a small application to demonstrate this;

Ludo Dice application

I am going to build a small application which allows us to create a new Die (singular for dice) and roll it. You can choose how many sides the die should have with only one rule;

  • The number of sides should be more than 1. Game of Ludo

dependencies

For the die to be fair, we need to generate random numbers every time a player rolls. Rust has a Rand crate which allows us to generate random numbers. In order to use it, we have to add it to our dependencies. So in out Cargo.toml file, we need to add the following line at the bottom;

[dependencies]
rand = "0.8.3"

implement

use rand::Rng;
 
struct Die {
   sides: u32,
}
 
impl Die {
   pub fn new(n_sides: u32) -> Die{
       if n_sides < 1{
           panic!("Die cannot have less than 1 side")
       }
       Die {
           sides: n_sides
       }
   }
   pub fn roll(&self) -> u32{
       let random_value = rand::thread_rng().gen_range(1..self.sides+1);
       random_value
   }
}
fn main() {
   let die = Die::new(6);
   println!("The dice 🎲  landed on {}",die.roll());
}

Testing

Now let's get to today's business. Inside our file lib.rs, we can create a new test;

#[cfg(test)]
mod tests {
   use super::*;
 
   #[test]
   fn roll_value_greater_than_zero(){
       let sides = 6;
       let die= Die::new(sides);
       assert!(die.roll() > 0)
   }
}

The first line #cfg(test) tells rust that the configuration that should be used here is for tests. Only run when you enter the command cargo test, and not cargo build.

The line with #[test] tells rust that the function below is a test function, not a normal function.

assert

The assert! macro, basically takes in a boolean. If it is false, it will cause the program to panic, making the test fail.

assert!(die.roll() > 0)

So here we are basically saying that, ensure that the value from the dice roll is always greater than zero.

Should Panic

In order to test if a function call will fail, we use should_panic annotation. Here we use it to test the business logic that we can't have a die with less than one side.

   #[test]
   #[should_panic()]
   fn sides_less_than_one(){
     let sides = 0;
     let die = Die::new(sides);
   }

If the function does not cause a panic as expected, the test should fail with the following error message;

failures:
 
---- tests::sides_less_than_one stdout ----
note: test did not panic as expected

Conclusion

After having now tested our Die application, we can now use it to play a nice game of Ludo with our friends.

Testing helps us have the confidence that our code will always work as expected. It is a habit worth adopting and Rust makes it easy to test our code.

Please let me know what you think of this series and what you'd like to see in the future.

View the full code on Github. Thanks for spending some time learning Rust with me.

Until next time. Peace ✌🏼 .

Share