Введение: Почему тестирование в 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