Введение: Почему тестирование в 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и проверка вывода. - Для встраиваемых систем: симуляция оборудования.