RustBrock/Test_Organization.md
2025-02-11 17:35:32 -07:00

6.0 KiB

Test Organization

The Rust community thinks about tests in terms of two main categories

  • Unit tests
  • Integration Tests

Unit tests are small and more focused, testing one module in isolation at a time and can test private interfaces

Integration tests are entirely external to your library and use your code in the same way any other external code would. Using only the public interface and potentially exercising multiple modules per test.

Unit Tests

The purpose of these kinds of tests is to test each unit of code in isolation from the rest of the code to pinpoint where the code is and isn't working as expected

You put unit tests in the src directory in each file the code that they are testing.

The convention is to create a module named tests in each file to contain the test functions and to annotate the module with cfg(test)

The Tests Module and #[cfg(test)]

The #[cfg(test)] annotation on the tests module tells Rust to compile and run the test code only when you run cargo test, not when you run cargo build.

This both saves time when compiling when you only want to build the library and saves space in the resultant compiled artifact because the tests are not included.

You will see that because integration tests go in a different directory, they don't need the #[cfg(test)] annotation.

However because unit tests go in the same files as the code you will use the #[cfg(test)] annotation to specify that they shouldn't be included in the compiled result

Recall when we generated the new adder project. Cargo generated this code for us:

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

On the auto generated tests module the attribute cfg stands for configuration and tells Rust that the following item should only be included given a certain config option

In this case the config option is test which is provided by Rust for compiling and running the tests.

Using the cfg attribute, Cargo compiles our test code only if we actively run the tests with cargo test

This includes any helper functions that might be within this module, and includes functions annotated with #[test]

Testing Private Functions

It is debated about whether or not private functions should be tested directly and other languages make it difficult or impossible to do so.

Regardless of which testing ideology you believe, Rust's privacy rules do allow you to test private functions

Consider this code

pub fn add_two(a: usize) -> usize {
    internal_adder(a, 2)
}

fn internal_adder(left: usize, right: usize) -> usize {
    left + right
}

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

    #[test]
    fn internal() {
        let result = internal_adder(2, 2);
        assert_eq!(result, 4);
    }
}

Note that internal_adder function is not marked as pub.

Tests are just Rust code, and the tests module is just another module.

Because the module is a child module it can use the items in their ancestor modules.

In this test we bring all of the tests module's parent items into the scope using the use super::* and then we can call the private function internal_adder

If you don't believe that internal functions should be tested there is nothing in Rust that will compel you to do so.

Integration Tests

In Rust integration tests are entirely external to library.

They use your library in the same way any any external code would, which means that they can only call functions that are part of your library's public API.

The purpose of these is to tests weather units work together correctly.

Units of code that work correctly on their own could have problems when put together, so test coverage of the integrated code is important as well.

To create integration tests you need a tests directory

The tests Directory

We create a tests directory at the top level of our project directory next to src

Cargo knows how to look for integration test files in this directory

We can make as many as we want and Cargo will compile each of the files as an individual crate

Here is how it should be setup and what your file directory system should look like

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

Here is what tests/integration_test.rs should contain

use adder::add_two;

#[test]
fn it_adds_two() {
    let result = add_two(2);
    assert_eq!(result, 4);
}

Each file in the tests directory is a separate crate so we need to bring our library into each test crat's scope

So we need to use use adder::add_two; in this case. We don't need unit tests because we aren't writing tests for them.

We don't need to annotate any code in tests/integration_test.rs with #[cfg(test)].

Cargo treats the tests directory specially and compiles files in this directly only when we run cargo test

Here would be the result output in this case

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

There are three sections of the output

  1. The Unit Tests
  2. The Integration test
  3. The Doc Tests

Note that if any test in a section fails the following sections will not run For example if a unit test fails, there wont be any output for integration and doc tests because those tests will only be run if all unit tests are passing.