Тестирование в Rust: unit, integration, e2e от А до Я

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

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

Начать курс

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

Rust — язык системного программирования, который славится своей безопасностью памяти и производительностью. Однако даже самый строгий компилятор не гарантирует, что ваша программа делает именно то, что нужно. Именно здесь на сцену выходит тестирование. В Rust встроенная поддержка тестов — одна из лучших среди современных языков. Вы можете писать unit-тесты (модульные), integration-тесты (интеграционные) и e2e-тесты (сквозные), не подключая сторонних библиотек.

В этой статье мы разберем все три уровня тестирования на практических примерах. Вы узнаете, как писать тесты, как их запускать с помощью cargo test, и как организовать тестовую инфраструктуру для больших проектов.

1. Unit-тесты в Rust: тестируем функции и модули

Unit-тесты проверяют работу отдельных функций или методов в изоляции. В Rust принято размещать unit-тесты прямо в файле с исходным кодом, внутри модуля tests с аннотацией #[cfg(test)]. Это позволяет тестам иметь доступ к приватным функциям модуля.

1.1. Первый unit-тест

Создадим простую библиотеку для вычислений и напишем для нее тесты.

// 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("Деление на ноль"))
    } 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(), "Деление на ноль");
    }
}

Запустите тесты командой cargo test. Вы увидите зеленые галочки, если все тесты пройдены.

1.2. Атрибуты тестов

  • #[test] — указывает, что функция является тестом.
  • #[should_panic] — тест считается пройденным, если функция вызвала панику.
  • #[ignore] — временно отключает тест.
#[test]
#[should_panic(expected = "panic message")]
fn test_panic() {
    panic!("panic message");
}

#[test]
#[ignore = "Тест отключен до исправления бага #42"]
fn test_expensive() {
    // Долгий тест
}

2. Integration-тесты: проверяем взаимодействие компонентов

Integration-тесты проверяют работу нескольких модулей или внешних зависимостей вместе. В Rust они размещаются в отдельной директории tests/ на уровне корня проекта. Каждый файл в этой папке — отдельный тестовый крейт, который видит только публичный API вашей библиотеки.

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

Создайте папку tests/ и файл tests/integration_test.rs:

// tests/integration_test.rs
use my_crate::add; // Имя вашего крейта

#[test]
fn test_add_integration() {
    // Проверяем, что функция работает как часть библиотеки
    let result = add(10, 20);
    assert_eq!(result, 30);
}

#[test]
fn test_divide_integration() {
    // Тест с несколькими вызовами
    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. Общие модули для интеграционных тестов

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

// tests/common/mod.rs
pub fn setup_test_environment() {
    // Инициализация логов, подключение к тестовой БД и т.д.
    println!("Тестовая среда настроена");
}
// tests/advanced_test.rs
mod common;

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

3. E2E-тесты (End-to-End): проверяем приложение целиком

E2E-тесты имитируют поведение пользователя и проверяют работу всего приложения от начала до конца. В Rust для этого часто используют отдельные бинарные файлы или запускают внешние процессы.

3.1. E2E для CLI-приложений

Если вы пишете консольную утилиту, можно протестировать ее через запуск процесса:

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

#[test]
fn test_cli_help() {
    let output = Command::new("cargo")
        .args(&["run", "--", "--help"])
        .output()
        .expect("Не удалось запустить команду");

    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("Usage"));
}

3.2. E2E для веб-сервисов

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

// tests/e2e_web.rs
#[test]
fn test_api_health() {
    // Предполагаем, что сервер запущен на localhost:8080
    let client = reqwest::blocking::Client::new();
    let response = client
        .get("http://localhost:8080/health")
        .send()
        .expect("Ошибка запроса");

    assert_eq!(response.status(), 200);
    let body: serde_json::Value = response.json().unwrap();
    assert_eq!(body["status"], "ok");
}

Важно: Обычно E2E-тесты запускаются отдельно от unit и integration, так как требуют запущенного приложения. Используйте #[ignore] или создайте отдельную команду cargo test --test e2e

Блоги

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