Тестирование в Rust: unit, integration, e2e — полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему тестирование в Rust — это просто и надёжно

Rust с самого начала проектировался как язык, который помогает писать безопасный и надёжный код. Однако даже самая строгая система типов не гарантирует правильности бизнес-логики. Именно здесь на помощь приходит тестирование. В Rust процесс написания тестов встроен прямо в инструментарий cargo, что делает его невероятно удобным и доступным.

В этой статье мы разберём три уровня тестирования: unit (модульное), integration (интеграционное) и e2e (end-to-end). Вы узнаете, как писать тесты, когда какой подход использовать и как автоматизировать проверку вашего кода. Материал будет полезен как новичкам, так и опытным разработчикам, переходящим на Rust.

Unit-тесты: Тестируем изнутри

Unit-тесты (модульные тесты) — это самый базовый и распространённый вид тестирования. Они проверяют отдельные функции, методы или модули в изоляции от остальной системы. В Rust принято писать unit-тесты прямо в том же файле, что и тестируемый код, или в отдельном файле tests.rs внутри модуля.

Как писать unit-тесты в Rust

Для создания теста используется атрибут #[test]. Тесты помещаются в модуль с атрибутом #[cfg(test)], который гарантирует, что тестовый код не попадёт в финальную сборку.

// src/lib.rs
pub 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");
    }
}

Запуск unit-тестов

Запустить тесты можно одной командой:

cargo test

Вывод покажет, сколько тестов пройдено, а сколько упало. Для запуска конкретного теста используйте фильтр:

cargo test test_add

Unit-тесты идеальны для проверки граничных случаев, обработки ошибок и корректности алгоритмов. Они быстрые и не требуют внешних зависимостей.

Integration-тесты: Проверяем взаимодействие модулей

Интеграционные тесты проверяют, как разные модули вашей библиотеки или приложения работают вместе. В отличие от unit-тестов, они не имеют доступа к внутренним функциям (если те не экспортированы как pub). Интеграционные тесты пишутся в отдельной папке tests/ на уровне корня проекта.

Структура интеграционных тестов

Создайте папку tests/ в корне вашего проекта и добавьте в неё файлы с тестами. Каждый файл будет скомпилирован как отдельный крейт.

// tests/integration_test.rs
use my_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);
}

Общий модуль для тестов

Если несколько интеграционных тестов используют одинаковые вспомогательные функции, вынесите их в общий модуль. Для этого создайте папку tests/common/mod.rs:

// tests/common/mod.rs
pub fn setup() {
    // инициализация логгера, базы данных и т.д.
    println!("Setting up test environment...");
}

// tests/another_test.rs
mod common;

#[test]
fn test_with_setup() {
    common::setup();
    // ваш тест
}

Интеграционные тесты — отличный способ проверить API вашей библиотеки и убедиться, что публичные функции работают корректно в связке.

E2E-тесты: Тестируем сценарии пользователя

End-to-end (e2e) тесты имитируют поведение реального пользователя и проверяют работу всей системы целиком: от ввода данных до вывода результата. Для веб-приложений это может быть запуск сервера и отправка HTTP-запросов, для CLI-утилит — вызов команд и проверка вывода.

Пример e2e теста для CLI-приложения

Предположим, у нас есть простая CLI-утилита, которая принимает два числа и выводит их сумму. E2e тест будет запускать бинарник и проверять его вывод.

// tests/e2e_test.rs
use std::process::Command;

#[test]
fn test_cli_add() {
    let output = Command::new("cargo")
        .args(&["run", "--", "5", "7"])
        .output()
        .expect("Failed to execute command");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Result: 12"), "Expected sum 12, got: {}", stdout);
}

#[test]
fn test_cli_invalid_input() {
    let output = Command::new("cargo")
        .args(&["run", "--", "abc", "5"])
        .output()
        .expect("Failed to execute command");

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("error"), "Expected error message");
}

E2E для веб-приложений

Для тестирования веб-сервисов на Rust (например, с использованием Actix-web или Axum) можно запустить сервер в фоне и отправлять запросы через reqwest:

use std::thread;
use std::time::Duration;

#[test]
fn test_api_endpoint() {
    // Запускаем сервер в отдельном потоке
    thread::spawn(|| {
        // ваш код запуска сервера
    });
    thread::sleep(Duration::from_secs(2)); // ждём старта

    let client = reqwest::blocking                

Блоги

Рекомендаций по книгам