As the saying goes, “If you write a test well, you’ll get a bonus.”

Experienced developers often use unit tests to ensure that the basic logic of the code is correct. If you are a new developer and haven’t yet experienced the benefits of unit testing, I suggest you read my previous article on Code Cleanliness: The Status of Unit Testing.

Writing unit tests generally requires three steps:

  1. Prepare test cases that cover as much code as possible
  2. Execute code that needs to be tested
  3. Decide if the result is the one you want

With that in mind, let’s take a look at how unit tests should be written in Rust.

First we set up a Library project

$ cargo new adder --lib
     Created library `adder` project
Copy the code

Then start writing the test code in the SRC /lib.rs file

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2.4); }}Copy the code

Running cargo Test on the command line at this point gives you the test results

As you can see, the results show that Rust ran a test and it passed. We will put down the doc-tests and talk about them later.

Of course, this is not our usual test. In daily development, we usually write our business code first, then unit test the various functions, and finally do integration tests on certain modules. So let’s simulate how tests should be written in a daily development process.

Unit testing

We are still using the above project. First, we will write a “business code” in SRC /lib.rs.

pub fn add_two(a: i32) - >i32 {
    internal_adder(a, 2)}fn internal_adder(a: i32, b: i32) - >i32 {
    a + b
}
Copy the code

This is a very simple code, the exposed function is just a function to add 2, internal call a function to add two numbers. Now let’s do a unit test on this inner function.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn internal() {
        assert_eq!(4, internal_adder(2.2)); }}Copy the code

In the test module, if you want to use the functions in our business code, you need to use super::*; Bring it into the available range. Next, we execute the cargo test again, and the test results are similar.

Unit testing is really about finding problems in the code, so let’s try a false test. Let’s say we want 2 plus 2 to be equal to 5.

Here our assert_eq! The left and right sides are not equal, causing thread panic and thus causing the test to fail. Note: run with ‘RUST_BACKTRACE=1’ environment variable to display a backtrace note: run with ‘RUST_BACKTRACE=1’ environment variable to display a backtrace Following this prompt, we set the variable RUST_BACKTRACE=1 and then execute cargo test.

Rust will print out the error stack, which is not the complete error stack, according to the results, and we can also set RUST_BACKTRACE to full to see more details. I’m not going to do the demo here.

Integration testing

Next, let’s demonstrate integration testing. We usually place integration tests in a separate directory. In the lib.rs file, the rust test mod is named tests. Similarly, we create the tests directory under SRC. The Tests directory contains all of our integration test code.

As shown, integration_test is the file where we test the code, and the mod.rs file in the common directory is the configuration necessary for some integration tests. Here we just put an empty setup function.

In integration testing, we need to test our code as if it were normal for others to use it, first by bringing our mods to use, and of course adding common’s mods.

use adder;

mod common;

#[tests]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
}
Copy the code

Then we can test our exposed functions.

Ok, we have the integration test method. Now let’s take a look at the doc-tests that we’ve been ignoring.

Document the test

We already know that comments in Rust are double slash //, and that if we want to publish library code like the one we just wrote to crate. IO for others to use, we need to add documentation that starts with a triple slash // and includes examples for others to refer to.

/// Adds two to the number given.
///
/// # Examples
///
/ / / ` ` `
/// let arg = 5;
/// let answer = adder::add_two(arg);
///
/// assert_eq! (7, answer);
/ / / ` ` `
pub fn add_two(a: i32) - >i32 {
    internal_adder(a, 2)}Copy the code

Now that I’ve documented the add_two function, we’ll execute cargo test again.

We now understand that doc-tests are examples of running our documentation.

Commonly used features

So far, we’ve seen how to write test code in Rust. Let’s look at some of the more common features.

Runs the specified test code

We certainly don’t run full unit tests every time during development, that would be a waste of time. Usually, after we have developed a feature, we write a unit test for it, and then run that test alone. So can Rust run a single unit test? The answer is yes.

If you are careful, you may have noticed that Rust test results are calculated separately for each test, and each test has its own name, such as IT_works and internal. Assuming that both functions exist in our code, you can use cargo test internal if you want to run the internal test separately.

You can also use this method to run multiple tests with similar names. If we call the test internal_A, it will also be executed when we execute the cargo test internal command.

Ignoring a test

When we have a test that takes a long time to execute, it is not easy to execute it. If you want to execute more than one test, you can specify a different list of names in the same way we mentioned above. You can also ignore this test.

Now THAT I don’t want to do an internal test, I just need to make the following changes to the code:

#[test]
#[ignore]
fn internal() {
  assert_eq!(4, internal_adder(2.2));
}
Copy the code

Run the test again, and the results are shown.

We find that internal testing has been neglected at this point.

Test exception

In addition to testing if the code logic is healthy, we sometimes need to test for exceptions, such as whether the program can return the exception we expect when it receives an invalid parameter.

Let’s start by looking at how to test a program to return an exception message.

Rust provides us with an annotation called should_panic. We can use it to test if the program returns an exception:

pub fn add_two(a: i32) - >i32 {
    internal_adder(a, 2)}fn internal_adder(a: i32, b: i32) - >i32 {
    if a < 0 {
        panic!("a should bigger than 0");
    }
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn internal() {
        assert_eq!(4, internal_adder(-2.2)); }}Copy the code

When we run the test, we will find that the internal test passes because it has thread panic, which is what we want.

In addition, we can also specify the exception we specifically expect, so we can add the expected parameter after should_panic.

#[test]
#[should_panic(expected = "a should be positive")]
fn internal() {
  assert_eq!(4, internal_adder(-2.2));
}
Copy the code

You can run this test code for yourself and see how it works.

conclusion

I’ve shown you how to do unit testing, integration testing, and, more specifically, documentation testing in Rust. Finally, three common test features are introduced.

As a final reminder, don’t start writing unit tests after you’ve written a bunch of features during development. You’ll probably give up because the testing code is too cumbersome. It is recommended that you start unit testing as soon as you write a feature so that you can immediately see how your code works and feel a sense of accomplishment. This is called “step by step”.