Rust Basic Testing
Tags: rust, testing
Published: 2021-04-19 06:26:02

As I work through learning **rust**, I figured it was time to to start doing actual testing.

## Why Test
Simple, it's really essential when building anything beyond a few lines of simple code. Being able to run a series of reproducible tests as you develop; allows you to ensure that changes you make are not breaking exisiting code. You also have the option to develop using **TDD**.  
Of course I have not adhered to any of this as I explore the basics of working with rust. But things are progressing and I should now see how testing basically works in the world of rust.

I am going to just cover the style of testing that I have played with so far. For more information, I refer to you to [the book](https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html). Also be aware this not about how to test code. It's more about how tests can be integrated into code as you develop in the rust environment.
## Basic inline testing
Let's start with the basic testing. Here we just add a test in the same file as the function we want to test.  
Looking at `dns/spf/mod.rs` we find the following code at the end of the file.
```rust
fn remove_qualifier(record: &str) -> &str {
    // Remove leading (+,-,~,?) character and return an updated str
    let mut chars = record.chars();
    chars.next();
    chars.as_str()
}
#[test]
fn test_remove_qualifier() {
    let test_str = "abc";
    let result = remove_qualifier(test_str);
    assert_eq!(result, "bc");
}
```
We have a function called `remove_qualifier()` which takes a reference to a `slice`.
According to the comment, it removes the leading `char` and returns a reference to the new str.  
Directly below that we have the `#[test]` followed by a new function called `test_remove_qualifier()`. This is the test function.
- It creates a slice containing *abc*
- Passes it to remove_qualifier which returns a slice back to `result`
- Then we check using an `assert_eq!` macro that `result` actually does match our expected slice of `bc`

We can run this test using **cargo test** in the terminal. I have removed some output from other tests that already exists.
#### Test Output
```zsh
> cargo test
snip...
test dns::spf::test_remove_qualifier ... ok
snip...
```

Let's take a look at some more examples.

In the same file we have the following function and a of tests. This function actually uses **remove_qualifier()**
```rust
// Check if the initial character in the string `record` matches `c`
// If they do no match then return the initial character
// if c matches first character of record, we can `+`, a blank modiifer equates to `+`
fn return_and_remove_qualifier(record: &str, c: char) -> (char, &str) {
    // Returns a tuple of (qualifier, &str)
    // &str will have had the qualifier character removed if it existed. The &str will be unchanged
    // if the qualifier was not present
    if c != record.chars().nth(0).unwrap() {
        // qualifier exists. return tuple of qualifier and `record` with qualifier removed.
        (record.chars().nth(0).unwrap(), remove_qualifier(record))
    } else {
        // qualifier does not exist, default to `+` and return unmodified `record`
        ('+', record)
    }
}
#[test]
fn test_return_and_remove_qualifier_no_qualifier() {
    let source = "no prefix";
    let (c, new_str) = return_and_remove_qualifier(source, 'n');
    assert_eq!('+', c);
    assert_eq!(source, new_str);
}
#[test]
fn test_return_and_remove_qualifier_pass() {
    let source = "+prefix";
    let (c, new_str) = return_and_remove_qualifier(source, 'n');
    assert_eq!('+', c);
    assert_eq!("prefix", new_str);
}
#[test]
fn test_return_and_remove_qualifier_fail() {
    let source = "-prefix";
    let (c, new_str) = return_and_remove_qualifier(source, 'n');
    assert_eq!('-', c);
    assert_eq!("prefix", new_str);
}
#[test]
fn test_return_and_remove_qualifier_softfail() {
    let source = "~prefix";
    let (c, new_str) = return_and_remove_qualifier(source, 'n');
    assert_eq!('~', c);
    assert_eq!("prefix", new_str);
}
#[test]
fn test_return_and_remove_qualifier_neutral() {
    let source = "?prefix";
    let (c, new_str) = return_and_remove_qualifier(source, 'n');
    assert_eq!('?', c);
    assert_eq!("prefix", new_str);
}
```
I should point out, that each test function is preceeded by *#[test]*. I have also seen it written as:
```rust
#[test] fn ......() {
  code 
}
```
  
These test the five possible cases, 
1. No prefix present. A default prefix of `+` is returned along with an unmodified slice
2. `+` prefix present, `+` and a modified slice is returned
3. `-` prefix present, `-` and a modified slice is returned
4. `~` prefix present, `~` and a modified slice is returned
5. `?` prefix present, `?` and a modified slice is returned

#### Test Output
```zsh
> cargo test
snip...
test dns::spf::test_return_and_remove_qualifier_fail ... ok
test dns::spf::test_return_and_remove_qualifier_neutral ... ok
test dns::spf::test_return_and_remove_qualifier_no_qualifier ... ok
test dns::spf::test_return_and_remove_qualifier_pass ... ok
test dns::spf::test_return_and_remove_qualifier_softfail ... ok
```

This is the quick easy way to introduce tests. The test will only be run when we run **cargo test** in the terminal. The down side here is, if you need to make use of a library for testing purposes only. Rust will complain about an unused library at compile time.
## Basic module testing

Another way to do testing is to create tests as a module.  
For this let's take a look at the `spf_test.rs` located under `src/dns/spf`

```rust
#[cfg(test)]

mod test_spf {

    use crate::dns::spf::Spf;

    #[test]
    fn test_redirect() {
        let input = "v=spf1 redirect=_spf.google.com";

        let mut spf = Spf::new(&input.to_string());
        assert_eq!(input, spf.source());
        spf.parse();
        assert_eq!(spf.is_redirect(), true);
        assert_eq!(spf.include.is_none(), true);
        assert_eq!(spf.a.is_none(), true);
        assert_eq!(spf.mx.is_none(), true);
        assert_eq!(spf.ip4.is_none(), true);
        assert_eq!(spf.ip6.is_none(), true);
    }
    #[test]
    fn test_hotmail() {
      let input = "v=spf1 ip4:157.55.9.128/25 include:spf.protection.outlook.com include:spf-a.outlook.com include:spf-b.outlook.com include:spf-a.hotmail.com include:_spf-ssg-b.microsoft.com include:_spf-ssg-c.microsoft.com ~all";

      let mut spf = Spf::new(&input.to_string());
      assert_eq!(input, spf.source());
      spf.parse();
      assert_eq!(spf.is_redirect(), false);
      assert_eq!(!spf.includes().as_ref().unwrap().is_empty(), true);
      assert_eq!(spf.includes().as_ref().unwrap().len(), 6);
      assert_eq!(
          spf.includes().as_ref().unwrap()[0].as_mechanism(),
          "include:spf.protection.outlook.com"
        );
        assert_eq!(spf.ip4().as_ref().unwrap().len(), 1);
        assert_eq!(
          spf.ip4().as_ref().unwrap()[0].as_mechanism(),
          "ip4:157.55.9.128/25"
        );
        assert_eq!(*spf.all(), '~');
      }
//snip...
}
```
Now there is an independent file for testing.
In order to test the **Spf** struct and functions we need to **use** it in the module. So we write `use crate::dns::spf::Spf` and we now have access to this code in this testing file.

Now we can write our series of test functions.  

I will just talk about the *hotmail* example here.
We have a known string which represents hotmail's current SPF record at the time of this article. It contains one **ip4** mechanism and 6 **include** mechanisms.  
I am checking the following items:
1. input and spf.source match
2. is_redirect() is false
3. includes() is NOT empty 
4. includes() contains **6** items
5. The first time **[0]** matches **include:spf.protection.outlook.com**
6. I then test the same things with ip4, 
   1. There is **one** item
   2. The first item matches **ip4:157.55.9.128/25**
7. Finally I check that the qualifier was also parsed correctly.

#### Test Output
```zsh
test dns::spf::spf_test::test_spf::test_hotmail ... ok
test dns::spf::spf_test::test_spf::test_netblocks2_google_com ... ok
test dns::spf::spf_test::test_spf::test_redirect ... ok
```

If you do not want to place tests in a single file, you can also place them in the source file of the module you wish to test. Take a look at `dns/spf/mechanism.rs`

### Conditional Compilation

For me, one of the big advantages I see is using the `mod` form for testing and conditional compilation.

During one of experiments, I thought it would be cool to test if two pointers were point to the same memory address. For this I needed `std::ptr::eq`. I figured I would just add it to the top of my imports. Then I could use it in my test functions.

Great that worked, but during a normal compile, rust would complain saying I had an unused import. This was because I was only making use of the library in my test functions and not in my normal functions. And the library was visible throughout the file.

The solution was to move the importing of the library into the testing module. Be aware this can not be done if you are just placing tests within the basic coding file as in the initial example.

```rust
#[cfg(test)]
mod SpfMechanismString {

    use super::SpfMechanism;
    use std::ptr; // Now scoped within SpfMechanismString
    //snip....
}
```
By positioning the import here. The library is only compiled into the code during testing and is not included during a normal compile. No more warning about an unused library.

## Conclusion
I know this is all pretty simple stuff. But it was something new to learn. The conditional compilation was probably the most interesting thing.

You can find the code related to this aritcle [here](https://github.com/Bas-Man/learning-rust-trust-dns-resolver/tree/BASIC-TESTING)