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

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

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

Начать курс

Введение в тестирование в Rust

Rust — это язык системного программирования, который славится своей безопасностью и производительностью. Однако даже самый надёжный код нуждается в проверке. Тестирование в Rust — это не просто опция, а неотъемлемая часть процесса разработки. Благодаря встроенной поддержке тестов в Cargo, вы можете легко писать и запускать unit, integration и e2e тесты. В этой статье мы разберём все три уровня тестирования на практических примерах.

Unit-тесты в Rust: основа надёжности

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

Структура unit-теста

Каждый unit-тест — это функция, помеченная атрибутом #[test]. Внутри неё вы используете макросы assert!, assert_eq! и assert_ne! для проверки результатов.

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

Обратите внимание на атрибут #[cfg(test)]. Он указывает компилятору включать этот модуль только при сборке тестов. Это экономит время компиляции финального бинарника.

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

Для запуска всех тестов в проекте используйте команду:

cargo test

Вы также можете запустить конкретный тест по имени:

cargo test test_add

Или отфильтровать тесты по ключевому слову:

cargo test divide

Интеграционные тесты в Rust: проверка взаимодействия

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

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

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

// tests/integration_test.rs
use my_project::math; // предполагаем, что библиотека называется my_project

#[test]
fn test_add_and_divide() {
    let sum = math::add(10, 5);
    let result = math::divide(sum as f64, 3.0).unwrap();
    assert_eq!(result, 5.0);
}

#[test]
fn test_complex_calculation() {
    let a = math::add(100, 200);
    let b = math::add(50, 50);
    let result = math::divide(a as f64, b as f64).unwrap();
    assert_eq!(result, 3.0);
}

Каждый файл в tests/ компилируется как отдельный крейт. Это означает, что вы можете иметь несколько файлов с тестами для разных сценариев.

Общий модуль для интеграционных тестов

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

// tests/common/mod.rs
pub fn setup() {
    // Инициализация тестового окружения
    println!("Setting up test environment...");
}

И используйте его в тестах:

// tests/integration_test.rs
mod common;

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

E2E тесты в Rust: полное погружение

E2E (end-to-end) тесты проверяют работу всего приложения от начала до конца. В Rust для этого часто используются внешние инструменты, такие как reqwest для HTTP-запросов или assert_cmd для тестирования CLI-приложений.

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

Добавьте в Cargo.toml зависимость:

[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.0"

Создайте тест в tests/cli_e2e.rs:

// tests/cli_e2e.rs
use assert_cmd::Command;
use predicates::prelude::*;

#[test]
fn test_cli_help() {
    let mut cmd = Command::cargo_bin("my_cli_app").unwrap();
    cmd.arg("--help")
        .assert()
        .success()
        .stdout(predicate::str::contains("Usage"));
}

#[test]
fn test_cli_add_command() {
    let mut cmd = Command::cargo_bin("my_cli_app").unwrap();
    cmd.args(["add", "5", "3"])
        .assert()
        .success()
        .stdout("Result: 8\n");
}

E2E тестирование веб-приложений

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

// tests/web_e2e.rs
use std::process::{Command, Child};
use std::time::Duration;
use std::thread;

fn start_server() -> Child {
    Command::new("cargo")
        .args(["run", "--bin", "my_web_app"])
        .spawn()
        .expect("Failed to start server")
}

#[test]
fn test_health_endpoint() {
    // Запускаем сервер в фоне
    let mut server = start_server();
    thread::sleep(Duration::from_secs(5)); // Ждём запуска

    // Делаем запрос
    let client = reqwest::blocking::Client::new();
    let response = client.get("http://localhost:8080/health")
        .send()
        .expect("Failed to send request");

    assert!(response.status().is_success());
    assert_eq!(response.text().unwrap(), "OK");

    // Останавливаем сервер
    server.kill().expect("Failed to kill server");
}

Практические советы по тестированию в Rust

Блоги

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