DEV Community

Cover image for Writing unit tests for rust applications
Bekka
Bekka

Posted on • Edited on

Writing unit tests for rust applications

Just like in any other programming language, we test our code to give the confidence to push code to production. Rust takes testing and documentation very seriously. This is also very crucial to open source development.

In this post, you will learn how to test your applications, the syntax and also scaling for bigger applications.

This article assumes you already have some rust experience with rust, if you don't, you can start your journey here.

Unit tests

You can write tests to test a function, a module or code snippet. This is usually written in the same module or the file that contains the code.

Rust has attributes for denoting tests, so that cargo will run this parts of the code conditionally. It is denoted #[cfg(test)]. The #[tests] attribute is always placed above the function that contains the test logic for each program you're testing. Let's see how this looks and works in rust.

We are building a word counter project. Create a new project, we will be using a binary project. (i.e the root will be main.rs). Let's call it "word_counter". Run this command in your terminal cargo new word_counter, replace the code with following code in your main.rs.


use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::prelude::BufRead;
use std::io::BufReader;

#[derive(Debug)]
struct WordCounter(HashMap<String, u64>);

impl WordCounter {
    fn new() -> WordCounter {
        WordCounter(HashMap::new())
    }
    fn increment(&mut self, word: &str) {
        let key = word.to_string();
        let count = self.0.entry(key).or_insert(0);
        *count += 1;
    }
    fn display(&self, filter: u64) {
        // keep data in a vector for sorting by storing it the same structure as the hashmap
        let mut vec_count: Vec<(&String, &u64)> = self.0.iter().collect();
        // sort by value
        vec_count.sort_by(|a, b| a.1.cmp(b.1));
        // print the sorted vector
        for (key, value) in vec_count {
            // print only the words that have a count greater than the filter
            if value > &filter {
                println!("{}: {}", key, value);
            }
        }
    }
}

fn main() {
    let arguments: Vec<String> = env::args().collect();
    if arguments.len() < 2 {
        panic!("Please provide a filename!");
    }
    let filename = &arguments[1];

    let file = File::open(filename).expect("Could not open file");

    let reader = BufReader::new(file);

    let mut word_counter = WordCounter::new();

    for line in reader.lines() {
        let line = line.expect("Could not read line");
        let words = line.split(" ");

        for word in words {
            if word == "" {
                continue;
            } else {
                word_counter.increment(&word);
            }
        }
    }
    word_counter.display(1);
}

Enter fullscreen mode Exit fullscreen mode

Create a .txt file(with any name) and put this in your outside your src directory, put in some random words and run this command cargo run your_file_name.txt. It should look like this.
running code rust from main

Now let's test our program. Add the following code into your main.rs.

#[cfg(test)]
// Our first test
#[test]
fn first_test() {
    assert!(true);
    assert_ne!(false, true);
    assert_eq!(1 + 1, 2);
}

Enter fullscreen mode Exit fullscreen mode

Rust has macros, what are macros? These are pieces of code that generate another piece of code. They allow us write code that writes code. It is also known as meta programming.

In the above code block, there are three macros in our unit test. assert! macro tests that the expression is true, it only contains one expression. assert_ne! tests that the expressions are not equal to each other, it takes in two expressions. assert_eq tests that the expressions are equal to each other. Run cargo tests first_test, this will run the test. There other macros, read more here.

Let's write another test. This tests if our program is correct.


// This is a test for the word counter
#[test]
fn test_word_count() {
    let mut word_counter = WordCounter::new();

    let random_words = [
        "this", "is", "just", "a", "list", "of", "random", "words.", "a", "word", "please",
    ];

    // this adds the words to the hashmap and increments the number of times the word appears
    for word in random_words.iter() {
        word_counter.increment(word);
    }

    assert!(random_words.len() > 0);

    // we should expect 10 because the word "a" is repeated twice, that means "a" will map to 2
    assert!(word_counter.0.len() == 10_usize);
}
Enter fullscreen mode Exit fullscreen mode

Run cargo test test_word_count to run this test. Notice that this command only runs this test. To run all tests in your module, run cargo test. You should see something like this
test command results

Integration tests

Integration tests are written in a separate directory. They consume the code/program and always placed in a different directory but at the root of the crate that contains the code.
Let's see how this works in code. Run cargo new integration_test_word_counter --lib in your crate root directory. We will be writing our word counter as a library instead. Note that you can't run cargo run in a library crate. It requires a bin directory(its root is main.rs).

We know the program works, we just want to practice integration tests. Create a new called tests directory outside the src directory. Create a file test_word_count.rs and paste the tests you wrote earlier. Run cargo test. You should see the following.
Running an integration test

We have a warning here because our crate is a lib project, we two options here, change the name of the main function or set another attribute to allow dead code. This means we can tell the compiler to ignore the code block that follows the attribute. Note that isn't a good practice in a production code, we are just doing this for learning purposes.

Add #[allow(dead_code)] right before the main function. Run cargo test. You shouldn't see any warning.

You just wrote your first unit tests and also an integration test!

Rust allows developers write tests and which are simple to construct and get running, if you noticed the compilation output, it also tests for documentations too!. This is will be covered in another article. Now you can test your rust programs and push safer programs to production.

Top comments (0)