Testing in Rust: Unit, Integration, E2E at an Advanced Level

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

Advanced Testing in Rust: from unit to E2E

Testing is an integral part of developing reliable system software in Rust. While basic concepts (simple test writing with #[test]) are familiar to everyone, advanced techniques — working with mocks, parameterization, custom attributes, and organizing E2E tests — require separate analysis. In this article, we will look at how to take Rust testing to a professional level using unit, integration, and end-to-end approaches.



1. Unit Testing: mock, parameterization, and custom attributes

Unit tests in Rust are usually written in the same file as the code, in the tests module. However, at an advanced level, there is a need to isolate the code under test from external dependencies.



Mock objects with traits

To create mocks in Rust, traits are most often used. You define a trait, implement it for the real object and for the test mock. Popular crates: mockall and mockito (for HTTP).

use mockall::automock;

#[automock]trait Database { fn get_user(&self, id: u32) -> String;}

fn process_user(db: &dyn Database, id: u32) -> String { let user = db.get_user(id); format!("User: {}", user)}

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

#[test] fn test_process_user() { let mut mock = MockDatabase::new(); mock.expect_get_user() .with(predicate::eq(42)) .returning(|_| "Alice".to_string()); assert_eq!(process_user(&mock, 42), "User: Alice"); }}


Parameterized tests

Repeating the same test with different input data is a common task. The rstest crate provides an elegant solution.

use rstest::rstest;

#[rstest]#[case(2, 3, 5)]#[case(0, 0, 0)]#[case(-1, 1, 0)]fn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) { assert_eq!(a + b, expected);}


Custom test attributes

Sometimes logic before or after a test is required. You can create your own attributes via procedural macros, but it is simpler to use the test-case or ctor crate for initialization.

// Example with ctor for logger initialization#[cfg(test)]mod tests {    use ctor::ctor;

#[ctor] fn init() { // Initialization once before all tests }}


2. Integration Testing: organization and working with external dependencies

Integration tests in Rust are located in the tests/ folder at the project root level. Each file there is a separate crate. Advanced techniques include common modules and testing with real databases or APIs.



Common utilities for integration tests

To avoid duplicating code (e.g., setting up a test server), create a common module inside tests/.

// tests/common/mod.rspub fn setup_test_db() -> String {    // Setting up a test DB    "connected".to_string()}

// tests/integration_test.rsmod common;

#[test]fn test_db_connection() { let db = common::setup_test_db(); assert_eq!(db, "connected");}


Working with HTTP requests

For testing HTTP clients, use mockito — it spins up a local mock server.

use mockito::{mock, server_url};

#[tokio::test]async fn test_api_call() { let _m = mock("GET", "/users/1") .with_status(200) .with_body(r#"{"id":1,"name":"Alice"}"#) .create();

let client = reqwest::Client::new(); let resp = client.get(&format!("{}/users/1", server_url())) .send() .await .unwrap(); assert!(resp.status().is_success());}


3. E2E Testing: full application cycle

End-to-end tests check the operation of the system as a whole: from start

Blogs

Book Recommendations