Testing in Rust: Unit, Integration, E2E from A to Z

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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                

Blogs

Book Recommendations