14 Testing and building your code from tests

This chapter covers

As your code grows, you’re going to want to think about its structure. The more you write, the more you’ll find that some code belongs in its own space, separate from other bits of code. You’ll also want to start testing your code as it grows because even Rust’s strict compiler can’t protect you from logic errors. Tests also help to remind you when you change your code if something has gone wrong. Writing tests can be a bit boring at times, but, in general, the more tests you have to catch problems, the better. We’ll also learn test-driven development (TDD), which means to write the tests before you write any code! In TDD, you write your tests, which will all fail. Only then, you write your code to make the tests pass one by one until finally everything works the way you intended.

14.1 Crates and modules

First, we are going to learn about where to put your code, what parts to make pub (available to others to use), and so on. Every time you write code in Rust, you are writing it in a crate. A crate is the file, or files, that go together for your code. (It also has a few other files to manage the project, but we’ll look at those later.) Inside the file you write, you can also make modules using the keyword mod. In other programming languages, a module is often known as a namespace. A module is a space for functions, structs, and anything else that you think belongs inside its own space. Here are some reasons to use a module:

You can probably see already why you might want to use modules as your code grows. So let’s make one and see what it looks like.

14.1.1 Module basics

To make a module, just write mod and start a code block with {}. We will make a module called print_things that has some printing-related functions:

mod print_things {
    use std::fmt::Display;
 
    fn prints_one_thing<T: Display>(input: T) {
        println!("{input}");
    }
}
 
fn main() {}

You can see that we wrote use std::fmt::Display; inside print_things because a module is a separate space. If you wrote use std::fmt::Display; on the very top outside of the print_things module, the code wouldn’t compile because it wouldn’t be able to find the path to the Display trait.

We also can’t call this function from main() yet. Without the pub keyword in front of fn it will stay private and inaccessible, so the code will not compile:

mod print_things {
    use std::fmt::Display;
 
    fn prints_one_thing<T: Display>(input: T) {
        println!("{}", input)
    }
}
 
fn main() {
    use print_things::prints_one_thing;
 
    prints_one_thing(6);
    prints_one_thing("Trying to print a string...".to_string());
}

Here’s the error:

error[E0603]: function `prints_one_thing` is private
  --> src\main.rs:10:30
   |
10 |     use crate::print_things::prints_one_thing;
   |                              ^^^^^^^^^^^^^^^^ private function
   |
note: the function `prints_one_thing` is defined here
  --> src\main.rs:4:5
   |
4  |     fn prints_one_thing<T: Display>(input: T) {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It’s easy to understand that the function prints_one_thing is private. The error message helpfully shows us with src\main.rs:4:5 where to find the function. This is helpful because you can write mods not just in one file but over a lot of files as well.

The solution to this is easy: we can just write pub fn instead of fn and now everything works:

mod print_things {
    use std::fmt::Display;
 
    pub fn prints_one_thing<T: Display>(input: T) {
        println!("{}", input)
    }
}
 
fn main() {
    use print_things::prints_one_thing;
 
    prints_one_thing(6);
    prints_one_thing("Trying to print a string...".to_string());
}

This prints

6
Trying to print a string...

The pub keyword works a little differently depending on what you are making public. Let’s see what the differences are.

14.1.2 More on how the pub keyword works

There are small differences in what the pub keyword does depending on whether it is in front of a struct, enum, trait, or module. These differences make sense when you think about them. They are as follows:

The Rust reference (http://mng.bz/XqGa) sums this up quite well in a single sentence: “By default, everything is private, with two exceptions: items in a pub Trait are public by default; Enum variants in a pub enum are also public by default.”

To demonstrate, let’s make a struct called Billy inside print_things. This struct will be almost all public, but not quite. The struct itself is public, so it will say pub struct Billy. Inside, it will have a name and times_to_print. The parameter name will not be public because we don’t want the user to be able to choose any name but Billy. But the user can select the number of times to print, so that part will be public. It looks like this:

mod print_things {
 
    #[derive(Debug)]
    pub struct Billy {                               
        name: String,
        pub times_to_print: u32,
    }
 
    impl Billy {
        pub fn new(times_to_print: u32) -> Self {    
            Self {
                name: "Billy".to_string(),           
                times_to_print,
            }
        }
        pub fn print_billy(&self) {
            for _ in 0..self.times_to_print {
                println!("{}", self.name);
            }
        }
    }
}
 
fn main() {
    use print_things::*;
 
    let my_billy = Billy::new(3);                    
    my_billy.print_billy();
}

Billy, the struct, is public, but the parameter name inside it is private.

The user needs to use new() to create a Billy. The user can only change the number of times_to_print.

We choose the name; the user can’t. No Billy struct can have any name but Billy.

Now we use *, which imports everything from the module print_things.

This will print

"Billy"
"Billy"
"Billy"

By the way, the * to import everything is called the glob operator. Glob stands for global—in other words, everything.

14.1.3 Modules inside modules

Inside a mod, you can create other modules. A child module (a module inside a module) can always use anything inside a parent module. You can see this in the next example where we have a mod city inside a mod province inside a mod country.

You can think of the structure like this: even if you are in a country, you might not be in a province (or state, or prefecture). And even if you are in a province, you might not be in a city. But if you are in a certain city, you are guaranteed to be in its province and in its country.

Two other things to pay attention to here are crate:: and super::. If you start a path to a type or function with crate::, it starts from the beginning—from the outside to the inside. But if you are inside a module, you can use super:: to move up one module. (As we learned before, the word super itself means “above,” like in “superior.”) Pay close attention to the city module. Inside this module, we are calling the same function twice, one using the path that starts from crate:: and the other using super:: two times to go up two modules. Those are simply two ways to call the same thing:

mod country {                                                         
    fn print_country(country: &str) {                                 
        println!("We are in the country of {country}");
    }
    pub mod province {                                                
        fn print_province(province: &str) {                           
            println!("in the province of {province}");
        }
        pub mod city {                                                
            pub fn print_city(country: &str, province: &str, city: &str) {
 
                crate::country::print_country(country);               
                super::super::print_country(country);       
 
                crate::country::province::print_province(province);   
                super::print_province(province);
                println!("in the city of {city}");
            }
        }
    }
}
 
fn main() {
    country::province::city::print_city("Canada", "New Brunswick", "Moncton");
}

The top level module doesn’t need pub.

This function isn’t pub.

Makes this module pub

This function isn’t pub either.

This module and the function it holds are both pub.

The path to the print_country function can be written from the crate level moving down or from the current location moving up using the keyword super.

Here’s one more example of writing the same thing either from the crate level down or current level up.

Try to follow the flow of the code and imagine what the output will be if you run this code sample.

The interesting part is that print_city() can access print_province() and print_country(). That’s because mod city is inside the other modules. It doesn’t need pub in front of print_province() to use it. And that makes sense: a city doesn’t need to do anything to be inside a province and inside a country.

Here is the output (see figure 14.1):

We are in the country of Canada
We are in the country of Canada
in the province of New Brunswick
in the province of New Brunswick
in the city of Moncton

Figure 14.1 Files that aren’t declared don’t exist as far as Rust is concerned...

When putting together your own project, the general setup looks like this: a main.rs file for the main function and related code and a lib.rs file. which is the library to hold types, functions, and so on that aren’t related to the main running of the software you are building. Of course, nothing is stopping you from putting everything inside main.rs if you really want.

Something interesting happens when creating separate files (for example, a file called functions.rs): Rust won’t even notice them! You can write all sorts of garbage inside this new file and—although your IDE might notice—the program will compile without any problems.

To have Rust notice them, go to lib.rs and declare them using the mod keyword. So if you make a functions.rs file, you have to type mod functions; inside lib.rs. Otherwise, Rust won’t see it. But once the file has been declared, Rust will see it, and it won’t compile the code anymore if there is a problem with it (see figure 14.2).

Figure 14.2 ...but declare the file and Rust will snap to attention.

We will look more at structuring a project in chapter 18, where we will learn about Rust on the computer instead of just in the Playground. But, for now, let’s leave it at that and learn how to write tests.

14.2 Testing

Testing is a good subject to learn now that we understand modules. Testing your code is easy in Rust because you can write tests right next to your code. You can create separate test files if you want, but you don’t have to if you don’t want to. Let’s look at the easiest way to start testing.

14.2.1 Just add #[test], and now it’s a test

The easiest way to start testing is to add #[test] above a function. Here is a simple one:

#[test]
fn two_is_two() {
    assert_eq!(2, 2);
}

But if you try to run it in the Playground with the Run button, it gives an error: error[E0601]: `main` function not found in crate `playground`. That’s because you don’t use Run for tests; you use Test. To run this in the Playground, click on ··· next to RUN on the top left and change it to TEST. Now if you click on it, it will run all of your tests. In this case, it will just be one. (If you have Rust installed already, you will type cargo test to do this instead of cargo run or cargo check.)

Also, you don’t use a main() function for tests: they go outside. You can outright delete the main() function and still run tests.

Here is the output of the previous test:

running 1 test
test two_is_two ... ok
 
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

One other point to note: test functions can’t take any arguments. So this won’t compile:

#[test]
fn test_that_wont_work(input: i32) {}

The compiler message is as clear as day: error: functions used as tests can not have any arguments. A test function is pretty similar to main() in this way.

So, how does the compiler know that the test passed? It’s pretty simple: if a test function does not panic, then it is a pass, and if it does panic, then it’s a failure. The assert_eq! macro will panic if the two arguments inside it don’t match, or you can use other ways to panic upon failure: .unwrap(), .expect(), the panic! macro, and so on.

The output for a passing test is pretty boring, so let’s see what happens when it panics.

14.2.2 What happens when tests fail

Let’s change assert_eq!(2, 2) to assert_eq!(2, 3) and see what we get. When a test fails, you get a lot more information:

running 1 test
test two_is_two ... FAILED
 
failures:
 
---- two_is_two stdout ----
thread 'two_is_two' panicked at src/lib.rs:3:5:
assertion `left == right` failed
  left: 2
 right: 3
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
 
 
failures:
    two_is_two
 
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.00s

assert_eq!(left, right) and assert!(bool) are probably the most common ways to test a function in Rust. For assert_eq!, if the left and right sides don’t match, it will panic and show that the values are different: left has 2 but right has 3.

The output for the assert! macro is almost the same:

#[test]
fn two_is_two() {
    assert!(2 == 3);
}

The output is

running 1 test
test two_is_two ... FAILED
 
failures:
 
---- two_is_two stdout ----
thread 'two_is_two' panicked at src/lib.rs:3:1:
assertion failed: 2 == 3
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
 
 
failures:
    two_is_two
 
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.00s

So what does RUST_BACKTRACE=1 mean? This is a setting on your computer that you can use to get a lot more detail when an assertion fails. These settings are known as environment variables. We will learn more about them in chapter 18, but, in the meantime, just remember that they can be found using the function std::env::var(). Let’s use this function to take a look at the default value for RUST_BACKTRACE:

fn main() {
    println!("{:?}", std::env::var("RUST_BACKTRACE"));
}

By default, that will print Err(NotPresent). But in the Playground, it’s easy to enable: click ··· next to STABLE and set backtrace to ENABLED. Or you can use the function set_var() to do the same thing: std::env::set_var("RUST_BACKTRACE", "1"); If you do that, it will give you a lot more information:

running 1 test
test two_is_two ... FAILED
 
failures:
 
---- two_is_two stdout ----
thread 'two_is_two' panicked at src/lib.rs:3:5:
assertion failed: 2 == 3
stack backtrace:
   0: rust_begin_unwind
             at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/std/
             src/panicking.rs:597:5
   1: core::panicking::panic_fmt
             at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/
             src/panicking.rs:597:5src/panicking.rs:72:14
   2: core::panicking::panic
             at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/
             src/panicking.rs:597:5src/panicking.rs:127:5
   3: playground::two_is_two
             at ./src/lib.rs:3:5
   4: playground::two_is_two::{{closure}}
             at ./src/lib.rs:2:17
   5: core::ops::function::FnOnce::call_once
             at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/
             src/panicking.rs:597:5src/ops/function.rs:250:5
   6: core::ops::function::FnOnce::call_once
             at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/
             src/panicking.rs:597:5src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose
src/panicking.rs:597:5 backtrace.
 
 
failures:
    two_is_two
 
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered
src/panicking.rs:597:5 out; finished in 0.05s

You don’t need to use a backtrace unless you really can’t find where the problem is. But, luckily, you don’t need to understand it all either. If you read from the bottom to the top, you will soon come across where the error happens: it’s on line 4 where it says playground—that’s where it talks about your code. Here’s that part again:

   4: playground::two_is_two
             at ./src/lib.rs:3:5
   5: playground::two_is_two::{{closure}}
             at ./src/lib.rs:2:1

You’ll also notice that the message tells us that we can set "RUST_BACKTRACE=full" for a “verbose backtrace” (a detailed backtrace). This used to be the default backtrace on Rust until it was improved with the less complicated output we just saw.

The verbose backtrace output is so verbose that it would take up a full page in this book. The Playground doesn’t have a button to enable a verbose backtrace, but we can use std::env::set_var() to set it. Give this code a try if you want to see just how verbose it is:

#[test]
fn two_is_two() {
    std::env::set_var("RUST_BACKTRACE", "full");
    assert!(2 == 3);
}

The output is indeed verbose: it’s about four times longer!

Now let’s turn backtrace off again and return to regular tests.

14.2.3 Writing multiple tests

Now, we’ll start writing multiple tests. Let’s put a few simple functions together, followed by test functions to make sure that they work. Here are a few:

fn return_two() -> i8 {
    2
}
#[test]
fn it_returns_two() {
    assert_eq!(return_two(), 2);
}
 
fn return_six() -> i8 {
    4 + return_two()
}
#[test]
fn it_returns_six() {
    assert_eq!(return_six(), 6)
}

Now it runs both:

running 2 tests
test it_returns_two ... ok
test it_returns_six ... ok
 
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

That’s not too hard.

Rust programmers often put their tests in their own modules. To do this, use the mod keyword to create a new module and add #[cfg(test)] above it (remember: cfg means “configure”). This attribute tells Rust not to compile it unless you are testing. You also need to continue to write #[test] above each test. This is because later on, when you install Rust, you can do more complicated testing. You will be able to run one test, or all of them, or run a few. Also, don’t forget to write use super::*; because the test module needs access to the functions above it. Now, it will look like this:

fn return_two() -> i8 {
    2
}
fn return_six() -> i8 {
    4 + return_two()
}
 
#[cfg(test)]
mod tests {
    use super::*;
 
    #[test]
    fn it_returns_six() {
        assert_eq!(return_six(), 6)
    }
    #[test]
    fn it_returns_two() {
        assert_eq!(return_two(), 2);
    }
}

This is the way you will usually see testing done in Rust and in other languages, too. You write your code first, then want to make sure that it behaves in the way it should, and then write some tests. This is probably human nature since the desire to create and the desire to get work done is so strong. But you can also do it the other way around by writing the tests first! Let’s take a look at how that works.

14.3 Test-driven development

You might see the words test-driven development (TDD) when reading about Rust or another language. TDD is a bit unique, and some people like it while others prefer something else (so it’s up to you how to test your own code).

TDD means writing tests first, all of which will fail! Only then you start writing the code. Then you start writing the code and keep doing that until all the tests pass. The tests then stay to show you if something goes wrong when you add to and rewrite your code later on. This is pretty easy in Rust because the compiler gives a lot of information about what to fix. Let’s write a small example of test-driven development and see what it looks like.

14.3.1 Building a calculator: Starting with the tests

Let’s imagine a calculator that takes user input as a String. To make the example as simple as possible, we’ll only let the calculator subtract (we’ll call it the Subtractor). If the user writes "5 - 6", it should return -1; if the user writes "15 - 6 - 7", it should return 2; if the user writes "1 -- 1", it should return 2, and so on. And because we are using TDD, we will start with test functions before a single line of code is written.

We won’t write the Subtractor yet, but we still need to give it a bit of thought so we can write tests that it will need to pass. The plan is to use a single function called math() to do everything. It will return an i32 (we won’t use floats).

For our Subtractor, the following five tests seem reasonable:

The absolute minimum code to make the tests is to have an empty Subtractor struct and a .math() method that returns an i32. For the .math() method, we’ll just have it return a random number like 6—we’ll think about it later. The first code looks like this:

struct Subtractor;
 
impl Subtractor {
    fn math(&mut self, input: &str) -> i32 {
        6
    }
}
 
#[test]
fn one_minus_two_is_minus_one() {
    let mut calc = Subtractor;               
    assert_eq!(calc.math("1 - 2"), -1);
}
#[test]
fn one_minus_minus_one_is_two() {
    let mut calc = Subtractor;
    assert_eq!(calc.math("1 - -1"), 2);
}
#[test]
fn three_minus_three_minus_three_minus_minus_three_is_zero() {
    let mut calc = Subtractor;
    assert_eq!(calc.math("3-3-3--3"), 0);
}
#[test]
fn eighteen_minus_nine_minus_nine_is_zero_even_with_characters_on_the_end() {
    let mut calc = Subtractor;
    assert_eq!(calc.math("18  - 9     -9-----"), 0);
}
#[test]
#[should_panic]                              
fn panics_when_characters_not_right() {
    let mut calc = Subtractor;
    calc.math("7 - seven");
}

So far, there’s nothing to mutate in Subtractor, but we plan to have it hold input and parse numbers, so it will be mutable from the start.

Note that this test is annotated with #[should_panic]. If it doesn’t panic, that’s a failure.

The first part of the test output simply tells us which tests passed or not:

running 5 tests
test eighteen_minus_nine_minus_nine_is_zero_even_with_characters_on_the_end
... FAILED
test nine_minus_three_minus_three_minus_three_is_zero ... FAILED
test one_minus_two_is_minus_one ... FAILED
test one_minus_minus_one_is_two ... FAILED
test panics_when_characters_not_right - should panic ... FAILED

Along with that is information per failed test on why it failed, such as thread 'tests ::one_minus_two_is_minus_one' panicked at src/lib.rs:10:5: assertion left == right` failed. We haven’t started the .math() method yet, so this output is still of no use to us.

You can also see that function names in tests are usually quite descriptive, like one_minus_two_is_minus_one. You can probably imagine why: as your code grows, you might end up making 10s or even 100s of tests, and descriptive test names let you understand right away which tests have failed.

Now it’s time to think about how to make the Subtractor. First, we will accept any number, the minus symbol, and empty spaces. We can represent this with a const called OKAY_CHARACTERS that contains all the possible input. To check input, we can use .chars() on the const to make an iterator of characters and .any() to panic with an error message if any characters aren’t contained in OKAY_CHARACTERS.

Now, the code before the tests looks like this:

const OKAY_CHARACTERS: &str = "1234567890- ";
 
struct Subtractor;
 
impl Subtractor {
    fn math(&mut self, input: &str) -> i32 {
            if input
            .chars()
            .any(|character| !OKAY_CHARACTERS.contains(character))
        {
            panic!("Please only input numbers, -, or spaces.");
        }
        6
    }
}

Running the tests gives us this result:

running 5 tests
test one_minus_minus_one_is_two ... FAILED
test one_minus_two_is_minus_one ... FAILED
test panics_when_characters_not_right - should panic ... ok
test six_minus_three_minus_three_minus_minus_three_is_zero ... FAILED
test eighteen_minus_nine_minus_nine_is_zero_even_with_characters_on_the_end
... FAILED

One test succeeded! Our .math() method will only accept proper input now. That was the easiest part. Now it’s time to actually put the Subtractor together.

14.3.2 Putting the calculator together

The first step in putting the Subtractor together is to think about what the .math() method should return and how. Instead of returning a 6 every time, it should return some total. To start, we’ll concentrate on the following:

Here is our new code:

const OKAY_CHARACTERS: &str = "1234567890- ";
 
#[derive(Default)]
struct Subtractor {
    total: i32,
    num_to_parse: String,
}
 
impl Subtractor {
    fn math(&mut self, input: &str) -> i32 {
            if input
            .chars()
            .any(|character| !OKAY_CHARACTERS.contains(character))
        {
            panic!("Please only input numbers, -, or spaces.");
        }
 
        let input = input
            .trim_end_matches(|x| "- ".contains(x))    
            .chars()
            .filter(|x| *x != ' ')
            .collect::<String>();
 
        for character in input.chars() {
            match character {
                '-' => {
                    let num = self.num_to_parse.parse::<i32>().unwrap();
                    self.total -= num;
                    self.num_to_parse.clear();
                }
                number => self.num_to_parse.push(number),
            }
        }
        self.total
    }
}
 
#[test]
fn one_minus_two_is_minus_one() {                      
    let mut calc = Subtractor::default();
    assert_eq!(calc.math("1 - 2"), -1);
}
#[test]
fn one_minus_minus_one_is_two() {
    let mut calc = Subtractor::default();
    assert_eq!(calc.math("1 - -1"), 2);
}
#[test]
fn three_minus_three_minus_three_minus_minus_three_is_zero() {
    let mut calc = Subtractor::default();
    assert_eq!(calc.math("3-3-3--3"), 0);
}
#[test]
fn eighteen_minus_nine_minus_nine_is_zero_even_with_characters_
on_the_end() {
    let mut calc = Subtractor::default();
    assert_eq!(calc.math("18  - 9     -9-----"), 0);
}
#[test]
#[should_panic]
fn panics_when_characters_not_right() {
    let mut calc = Subtractor::default();
    calc.math("7 - seven");
}

.trim_end_matches() removes anything that matches at the end of a &str.

The tests are the same except that we are using Default to make the Subtractor now.

NOTE .trim_end_matches() and .trim_start_matches() used to be .trim_ right_matches() and .trim_left_matches(). But people noticed that some languages go from right to left (Persian, Hebrew, etc.) so right and left didn’t always mean end and start. You might still see the other names in really old Rust code.

The tests won’t change from here on, so we won’t include the test code in the code samples anymore. Happily, one more test passes!

running 5 tests
test eighteen_minus_nine_minus_nine_is_zero_even_with_characters_on_the_end
... FAILED
test one_minus_minus_one_is_two ... FAILED
test panics_when_characters_not_right - should panic ... ok
test three_minus_three_minus_three_minus_minus_three_is_zero ... FAILED
test one_minus_two_is_minus_one ... ok

We still haven’t made the Subtractor smart enough to know that a minus sign can also mean to add, so three tests have still failed. But interestingly, two of the tests have given us an unexpected hint for what to do next. Here is the error:

---- one_minus_minus_one_is_two stdout ----
thread 'one_minus_minus_one_is_two' panicked at src/lib.rs:22:44:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: Empty }

The code is still simple enough that we can imagine what is happening here. In this test, the input is "1 - -1". The spaces and needless input at the end are removed, turning the input to "1--". If we follow the logic, here is what the program is doing:

We can fix this with a quick check to see whether num_to_parse is empty or not. Change the scope that starts with for character in input.chars() to the following:

        for character in input.chars() {
            match character {
                '-' => {
                    if !self.num_to_parse.is_empty() {
                        let num = self.num_to_parse.parse::<i32>().unwrap();
                        self.total -= num;
                        self.num_to_parse.clear();
                    }
                }
                number => self.num_to_parse.push(number),
            }
        }

With that done, three tests still fail. But at least we are not trying to parse an empty string anymore, and the ParseIntErrors are gone. And it was thanks to the tests that we noticed this.

Up next, we will tell the Subtractor when it should add and when it should subtract. Fortunately, this isn’t too hard: one minus sign means subtract, two minus signs means to add, three means to subtract, and so on. We could count the number of minus signs, but there is a way that is both easier to use and to read: use an enum. We will make an enum called Operation with two variants: Add and Subtract. The Subtractor will default to Add, and every time it sees a minus sign, it will simply switch.

Let’s give this a try:

const OKAY_CHARACTERS: &str = "1234567890- ";
 
#[derive(Default)]
struct Subtractor {
    total: i32,
    num_to_parse: String,
    operation: Operation,
}
 
#[derive(Default)]
enum Operation {
    #[default]                                                    
    Add,
    Subtract,
}
 
impl Subtractor {
    fn switch_operation(&mut self) {
        self.operation = match self.operation {
            Operation::Add => Operation::Subtract,
            Operation::Subtract => Operation::Add,
        }
    }
    fn math(&mut self, input: &str) -> i32 {
        if input
            .chars()
            .any(|character| !OKAY_CHARACTERS.contains(character))
        {
            panic!("Please only input numbers, -, or spaces.");
        }
 
        let input = input
            .trim_end_matches(|x| "- ".contains(x))
            .chars()
            .filter(|x| *x != ' ')
            .collect::<String>();
 
        for character in input.chars() {
            match character {
                '-' => {
                    if !self.num_to_parse.is_empty() {
                        let num = self.num_to_parse.parse::<i32>().unwrap();
                        match self.operation {
                            Operation::Add => self.total += num,
                            Operation::Subtract => self.total -= num
                        }
                        self.operation = Operation::Add;          
                        self.num_to_parse.clear();                
                    }
                    self.switch_operation();
                }
                number => self.num_to_parse.push(number),
            }
        }
        self.total
    }
}

Since Rust 1.62 (released July 2022), you can now pick a default variant for an enum, as long as it is a "unit enum variant" (has no data in it). You do it by using the #[derive(Default)] attribute on top and then #[default] over the default variant.

These two lines restore the Subtractor to the default now that the operation is over.

Interestingly, now only one test passes! Let’s look at the failures closely (left = test output, right = expected output). See whether you can tell what they all have in common:

Input: "18  - 9     -9--  ---"
left: 9, right: 0
 
Input: "1 - 2"
left: 1, right: -1
 
Input: "1 - -1"
left: 1, right: 2
 
"3-3-3--3"
left: -3, right: 0

Ah, yes, they are all ignoring the last number. At the very end of our iterator through self.input, we always have a final number but just push it to self.num_to_parse and end the program without adding or subtracting it. To fix this, we can just check at the end whether num_to_parse is empty or not, and if it isn’t empty, we can add to or subtract from the total. Since that operation will use the same code as before, we can make a method called .do_operation() so that we aren’t duplicating code.

And after doing this, the tests pass. Here is the final code:

const OKAY_CHARACTERS: &str = "1234567890- ";
 
#[derive(Default)]
struct Subtractor {
    total: i32,
    num_to_parse: String,
    operation: Operation,
}
 
#[derive(Default)]
enum Operation {
    #[default]
    Add,
    Subtract,
}
 
impl Subtractor {
    fn switch_operation(&mut self) {
        self.operation = match self.operation {
            Operation::Add => Operation::Subtract,
            Operation::Subtract => Operation::Add,
        }
    }
 
    fn do_operation(&mut self) {
        let num = self.num_to_parse.parse::<i32>().unwrap();
        match self.operation {
            Operation::Add => self.total += num,
            Operation::Subtract => self.total -= num,
        }
        self.operation = Operation::Add;
        self.num_to_parse.clear();
    }
 
    fn math(&mut self, input: &str) -> i32 {
        if input
            .chars()
            .any(|character| !OKAY_CHARACTERS.contains(character))
        {
            panic!("Please only input numbers, -, or spaces.");
        }
 
        let input = input
            .trim_end_matches(|x| "- ".contains(x))
            .chars()
            .filter(|x| *x != ' ')
            .collect::<String>();
 
        for character in input.chars() {
            match character {
                '-' => {
                    if !self.num_to_parse.is_empty() {
                        self.do_operation();
                    }
                    self.switch_operation();
                }
                number => self.num_to_parse.push(number),
            }
        }
        if !self.num_to_parse.is_empty() {
            self.do_operation();
        }
        self.total
    }
}

Success! And now that the tests pass, we could start refactoring the code a bit. We could return a Result instead of panicking or make some small methods to make the code cleaner. But this section is only about testing, so we’ll leave it the way it is.

You can see that there is a back-and-forth process in TDD. It’s something like this:

Of course, tests don’t check everything, and it is wrong to think that “passing all tests” equals “the code is perfect.” At the end of the day, a test only checks what the human programmer thinks should be checked. But tests are also great for when you change your code. Say you change your code later on and run the tests. If one of them doesn’t work, you will know what to fix. This is especially important when working on a team or writing code that someone else might have to manage one day.

In this chapter, we’ve learned about structuring and testing your project, and we haven’t even needed to install Rust yet! This will be good practice for later on in the book when it’s time to move on to Rust installed on your computer. But in the meantime, we have a lot of Rust left that can be learned on the Playground. In the next chapter, we will learn some interesting patterns, plus a popular trait called Deref that gives you all the methods from someone else’s type inside your types for free!

Summary