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

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

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

Начать курс

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

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

В этой статье мы разберем три уровня тестирования в Rust: от изолированных unit-тестов до полноценных e2e-сценариев. Вы узнаете, как писать эффективные тесты, какие инструменты использовать и как организовать тестовую инфраструктуру. Материал будет полезен как новичкам, только начинающим изучать Rust, так и опытным разработчикам, желающим систематизировать знания.

1. Unit-тестирование: основа основ

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

1.1. Структура и атрибуты

Для создания unit-теста используется атрибут #[test]. Функция, помеченная им, не принимает аргументов и ничего не возвращает (или возвращает Result). Если внутри функции происходит паника (panic), тест считается проваленным.

// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, -1), -2);
    }
}

Обратите внимание на #[cfg(test)]. Этот атрибут гарантирует, что модуль с тестами будет скомпилирован только при запуске тестов (cargo test). В production сборку он не попадет.

1.2. Макросы для проверок

Rust предоставляет несколько макросов для проверки утверждений:

  • assert!(expr) — проверяет, что выражение истинно.
  • assert_eq!(left, right) — проверяет равенство (требует PartialEq и Debug).
  • assert_ne!(left, right) — проверяет неравенство.
  • panic!("message") — принудительно вызывает панику (используется для тестирования ошибок).

1.3. Тестирование паник с should_panic

Если ваша функция должна паниковать при некорректных входных данных, используйте атрибут #[should_panic]:

pub fn divide(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        panic!("Division by zero");
    }
    a / b
}

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

    #[test]
    #[should_panic(expected = "Division by zero")]
    fn test_divide_by_zero() {
        divide(10.0, 0.0);
    }
}

Параметр expected не обязателен, но он делает тест точнее: паника должна содержать указанную строку.

2. Интеграционное тестирование: проверка взаимодействия модулей

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

2.1. Создание интеграционного теста

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

// tests/integration_test.rs
use my_crate::add; // предполагаем, что ваш крейт называется my_crate

#[test]
fn test_add_integration() {
    let result = add(5, 10);
    assert_eq!(result, 15);
}

Чтобы запустить только интеграционные тесты, используйте команду:

cargo test --test integration_test

2.2. Общие модули для тестов

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

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

// tests/other_test.rs
mod common;

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

2.3. Интеграционные тесты для бинарных крейтов

Если ваш проект — бинарный (с main.rs), а не библиотечный, интеграционные тесты не смогут напрямую импортировать функции из main.rs. Решение: вынести логику в библиотеку (lib.rs) и импортировать её. main.rs должен оставаться максимально тонким.

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

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

3.1. Подходы к e2e в Rust

Поскольку Rust — системный язык, e2e-тестирование может выглядеть по-разному:

  • Для веб-приложений: запуск сервера (например, с помощью actix-web или axum) и отправка HTTP-запросов через reqwest.
  • Для CLI-утилит: запуск бинарника через std::process::Command и проверка вывода.
  • Для встраиваемых систем: симуляция оборудования.

3.2. Пример e2e-теста для веб-сер

Блоги

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