Writing unit tests for rust applications
Tags: rust, testing, development, beginners
Published: 2023-05-10 14:09:47

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.](https://doc.rust-lang.org/book)

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

```rust

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);
}

```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmw7sdm42en798w35ucq.png)

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

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

```

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.](https://doc.rust-lang.org/book/ch19-06-macros.html)

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

```rust

// 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);
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kt9vk5ojqdf5j6ogti2t.png)


### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ho06h2rtns5y84b0adrj.png)

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.