Rust Basic Testing

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. 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.

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

> 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()

// 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:

#[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

> 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

#[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

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.

#[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


See also