Introduction: Why Testing in Rust is the Foundation of Reliable Code
Rust is a systems programming language renowned for its memory safety and performance. However, even the strictest compiler does not guarantee that your program does exactly what is needed. This is where testing comes into play. In Rust, built-in test support is among the best of modern languages. You can write unit tests, integration tests, and e2e tests (end-to-end) without needing third-party libraries.
In this article, we will break down all three levels of testing with practical examples. You will learn how to write tests, how to run them using cargo test, and how to organize test infrastructure for large projects.
1. Unit Tests in Rust: Testing Functions and Modules
Unit tests verify the operation of individual functions or methods in isolation. In Rust, it is common to place unit tests directly in the source code file, inside a tests module with the #[cfg(test)] attribute. This allows tests to access private functions of the module.
1.1. Your First Unit Test
Let's create a simple calculation library and write tests for it.
// 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_divide_success() { let result = divide(10.0, 2.0).unwrap(); assert_eq!(result, 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"); }}Run the tests with the command cargo test. You will see green checkmarks if all tests pass.
1.2. Test Attributes
#[test]— indicates that the function is a test.#[should_panic]— the test is considered passed if the function panics.#[ignore]— temporarily disables the test.
#[test]#[should_panic(expected = "panic message")]fn test_panic() { panic!("panic message");}
#[test]#[ignore = "Test disabled until bug #42 is fixed"]fn test_expensive() { // Long-running test}2. Integration Tests: Checking Component Interaction
Integration tests verify the operation of multiple modules or external dependencies together. In Rust, they are placed in a separate tests/ directory at the project root level. Each file in this folder is a separate test crate that only sees the public API of your library.
2.1. Structure of Integration Tests
Create a tests/ folder and a file tests/integration_test.rs:
// tests/integration_test.rsuse my_crate::add; // Your crate's name
#[test]fn test_add_integration() { // Check that the function works as part of the library let result = add(10, 20); assert_eq!(result, 30);}
#[test]fn test_divide_integration() { // Test with multiple calls let a = 100.0; let b = 4.0; let result = my_crate::divide(a, b).unwrap(); assert!(result > 0.0); assert_eq!(result, 25.0);}2.2. Shared Modules for Integration Tests
If you need to reuse code between integration tests, create a file tests/common/mod.rs. This will not be treated as a test file, but you will be able to import it from other tests.
// tests/common/mod.rspub fn setu