Introduction: Why Testing in Rust is Simple and Reliable
Rust was designed from the start as a language that helps write safe and reliable code. However, even the strictest type system does not guarantee the correctness of business logic. This is where testing comes to the rescue. In Rust, the process of writing tests is built directly into the cargo toolchain, making it incredibly convenient and accessible.
In this article, we will cover three levels of testing: unit, integration, and e2e (end-to-end). You will learn how to write tests, when to use each approach, and how to automate checking your code. The material will be useful for both beginners and experienced developers transitioning to Rust.
Unit Tests: Testing from the Inside
Unit tests are the most basic and common type of testing. They check individual functions, methods, or modules in isolation from the rest of the system. In Rust, it is common to write unit tests directly in the same file as the code being tested, or in a separate tests.rs file within the module.
How to Write Unit Tests in Rust
To create a test, use the #[test] attribute. Tests are placed in a module with the #[cfg(test)] attribute, which ensures that the test code is not included in the final build.
// src/lib.rspub fn add(a: i32, b: i32) -> i32 { a + b}
pub fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err(String::from("Division by zero")) } else { Ok(a / b) }}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_add() { assert_eq!(add(2, 3), 5); }
#[test] fn test_add_negative() { assert_eq!(add(-1, 1), 0); }
#[test] fn test_divide_success() { let result = divide(10.0, 2.0); assert!(result.is_ok()); assert_eq!(result.unwrap(), 5.0); }
#[test] fn test_divide_by_zero() { let result = divide(10.0, 0.0); assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Division by zero"); }}Running Unit Tests
You can run tests with a single command:
cargo testThe output will show how many tests passed and how many failed. To run a specific test, use a filter:
cargo test test_addUnit tests are ideal for checking edge cases, error handling, and algorithm correctness. They are fast and do not require external dependencies.
Integration Tests: Checking Module Interaction
Integration tests check how different modules of your library or application work together. Unlike unit tests, they do not have access to internal functions (unless they are exported as pub). Integration tests are written in a separate tests/ folder at the root level of the project.
Structure of Integration Tests
Create a tests/ folder in the root of your project and add test files to it. Each file will be compiled as a separate crate.
// tests/integration_test.rsuse my_crate; // name of your crate
#[test]fn test_add_integration() { assert_eq!(my_crate::add(10, 20), 30);}
#[test]fn test_divide_integration() { let result = my_crate::divide(100.0, 4.0); assert_eq!(result.unwrap(), 25.0);}Common Module for Tests
If multiple integration tests use the same helper functions, extract them into a common module. To do this, create a tests/common/mod.rs folder:
// tests/common/mod.rspub fn setup() { // initialization of logger, database, etc. println!("Setting up test environment...");}
// tests/another_test.rsmod common;
#[test]fn test