How to Write Tests in Rust?

Introduction

Rust, a modern systems programming language, includes a powerful built-in testing framework. This framework helps developers create thorough test suites and supports test-driven development (TDD), where tests guide the coding process. Testing ensures code works correctly and remains reliable over time. Rust's testing tools are integrated into the language, making it easy to write, organize, and run tests.

Test

Testing Module in Rust

Rust uses the '#[cfg(test)]' attribute for testing. This means test code is only included during test runs, not in the final production build. This approach keeps test-specific code separate from the main codebase, preventing it from affecting the final executable.

Setting up a New Rust Project with Testing Support

Setting up a new Rust project with testing support is straightforward. When creating a new Rust project using Cargo, the Rust package manager, the project structure automatically includes support for testing. Now create a new project by running the following command.

cargo new test_project

This command creates a new directory named `test_project` with the following structure.

test_project/
├── Cargo.toml
└── src
    └── main.rs

The 'Cargo.toml' file contains the project metadata, and 'src/main.rs' is where your main application code resides. Let's add some tests to this project.

Writing Basic Tests

To write tests in Rust, you place your test functions within a module annotated with '#[cfg(test)]'. This module is usually placed at the bottom of your source file. Here's an example of how to write a basic test.

// src/main.rs

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

fn divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        None
    } else {
        Some(a / b)
    }
}

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

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    fn test_subtract() {
        assert_eq!(subtract(5, 3), 2);
        assert_eq!(subtract(2, 2), 0);
    }

    #[test]
    fn test_multiply() {
        assert_eq!(multiply(2, 3), 6);
        assert_eq!(multiply(-2, 3), -6);
    }

    #[test]
    fn test_divide() {
        assert_eq!(divide(6, 3), Some(2));
        assert_eq!(divide(1, 0), None);
    }

    #[test]
    #[should_panic]
    fn test_add_panic() {
        assert_eq!(add(2, 2), 5); // This test will panic because the assertion fails
    }

    #[test]
    #[should_panic]
    fn test_divide_panic() {
        let result = divide(1, 0).unwrap(); // This should panic because we are unwrapping a None
        assert_eq!(result, 0);
    }
}

In this example, we define a simple 'add' function and two tests for it. The '#[test]' attribute marks a function as a test function. The '#[should_panic]' attribute indicates that the test should panic.

Running Tests

To run tests in your Rust project, use the following Cargo command.

cargo test

Cargo will compile your code (including tests) and run all the tests. The output will indicate which tests passed and which failed.

Output

Test-result

Organizing Tests

For larger projects, it's beneficial to organize tests into separate files and directories. You can create a 'tests' directory at the root of your project to hold integration tests. Here's how to structure your project for better test organization.

test_project/
├── Cargo.toml
├── src
│   ├── main.rs
│   └── lib.rs
└── tests
    ├── common.rs
    ├── integration_test.rs
    └── additional_tests.rs
  • src/main.rs: Contains your main application code.
  • src/lib.rs: Contains your project's functions code.
  • tests/common.rs: Contains common setup code for your tests.
  • tests/integration_test.rs: Contains your integration tests.
  • tests/additional_tests.rs: Contains your additional tests.

Writing Integration Tests

Navigate to the 'src' directory open the 'lib.rs' file in your favorite editor and write your project functions like the following.

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

pub fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

pub fn divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        None
    } else {
        Some(a / b)
    }
}

In this file, we have defined basic arithmetic functions.

Now navigate to the 'tests' directory and edit the 'common.rs' file as follows.

// This file can contain common setup code for your tests.
pub fn setup() {
    // Common setup code, like initializing a database connection.
    println!("Setting up common test environment...");
}

Edit the 'integration_test.rs' file as follows.

use integration_test::{add, subtract};
mod common;

#[test]
fn test_add_integration() {
    common::setup();
    assert_eq!(add(5, 5), 10);
    assert_eq!(add(-1, 1), 0);
}

#[test]
fn test_subtract_integration() {
    common::setup();
    assert_eq!(subtract(10, 5), 5);
    assert_eq!(subtract(5, 5), 0);
}

In this file, we have defined the test case for the 'addition' and 'subtraction' functions.

Now edit the 'additional_tests.rs' file as follows.

use integration_test::{multiply, divide};
mod common;

#[test]
fn test_multiply_integration() {
    common::setup();
    assert_eq!(multiply(3, 4), 12);
    assert_eq!(multiply(-2, 3), -6);
}

#[test]
fn test_divide_integration() {
    common::setup();
    assert_eq!(divide(8, 2), Some(4));
    assert_eq!(divide(5, 0), None);
}

In this file, we have defined the test case for the 'multiply' and 'divide' functions.

Now start the testing by running the following command in the terminal.

cargo test

Output

Integration-test-result

Conclusion

Rust's built-in testing framework provides a robust and convenient way to ensure your code works as expected. By using '#[cfg(test)]', '#[test]', and organizing your tests effectively, you can maintain a high level of code quality and reliability. Whether you are writing unit tests, integration tests, or adopting a test-driven development approach, Rust's testing capabilities make it easier to create and manage your test suites.


Similar Articles