Введение: Почему тестирование в Rust — это просто и надёжно
Rust с самого начала проектировался как язык, который помогает писать безопасный и надёжный код. Однако даже самая строгая система типов не гарантирует правильности бизнес-логики. Именно здесь на помощь приходит тестирование. В Rust процесс написания тестов встроен прямо в инструментарий cargo, что делает его невероятно удобным и доступным.
В этой статье мы разберём три уровня тестирования: unit (модульное), integration (интеграционное) и e2e (end-to-end). Вы узнаете, как писать тесты, когда какой подход использовать и как автоматизировать проверку вашего кода. Материал будет полезен как новичкам, так и опытным разработчикам, переходящим на Rust.
Unit-тесты: Тестируем изнутри
Unit-тесты (модульные тесты) — это самый базовый и распространённый вид тестирования. Они проверяют отдельные функции, методы или модули в изоляции от остальной системы. В Rust принято писать unit-тесты прямо в том же файле, что и тестируемый код, или в отдельном файле tests.rs внутри модуля.
Как писать unit-тесты в Rust
Для создания теста используется атрибут #[test]. Тесты помещаются в модуль с атрибутом #[cfg(test)], который гарантирует, что тестовый код не попадёт в финальную сборку.
// 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("Division by zero"))
} else {
Ok(a / b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
}
#[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(), "Division by zero");
}
}
Запуск unit-тестов
Запустить тесты можно одной командой:
cargo test
Вывод покажет, сколько тестов пройдено, а сколько упало. Для запуска конкретного теста используйте фильтр:
cargo test test_add
Unit-тесты идеальны для проверки граничных случаев, обработки ошибок и корректности алгоритмов. Они быстрые и не требуют внешних зависимостей.
Integration-тесты: Проверяем взаимодействие модулей
Интеграционные тесты проверяют, как разные модули вашей библиотеки или приложения работают вместе. В отличие от unit-тестов, они не имеют доступа к внутренним функциям (если те не экспортированы как pub). Интеграционные тесты пишутся в отдельной папке tests/ на уровне корня проекта.
Структура интеграционных тестов
Создайте папку tests/ в корне вашего проекта и добавьте в неё файлы с тестами. Каждый файл будет скомпилирован как отдельный крейт.
// tests/integration_test.rs
use my_crate; // имя вашего крейта
#[test]
fn test_add_integration() {
assert_eq!(my_crate::add(10, 20), 30);
}
#[test]
fn test_divide_integration() {
let result = my_crate::divide(100.0, 4.0);
assert_eq!(result.unwrap(), 25.0);
}
Общий модуль для тестов
Если несколько интеграционных тестов используют одинаковые вспомогательные функции, вынесите их в общий модуль. Для этого создайте папку tests/common/mod.rs:
// tests/common/mod.rs
pub fn setup() {
// инициализация логгера, базы данных и т.д.
println!("Setting up test environment...");
}
// tests/another_test.rs
mod common;
#[test]
fn test_with_setup() {
common::setup();
// ваш тест
}
Интеграционные тесты — отличный способ проверить API вашей библиотеки и убедиться, что публичные функции работают корректно в связке.
E2E-тесты: Тестируем сценарии пользователя
End-to-end (e2e) тесты имитируют поведение реального пользователя и проверяют работу всей системы целиком: от ввода данных до вывода результата. Для веб-приложений это может быть запуск сервера и отправка HTTP-запросов, для CLI-утилит — вызов команд и проверка вывода.
Пример e2e теста для CLI-приложения
Предположим, у нас есть простая CLI-утилита, которая принимает два числа и выводит их сумму. E2e тест будет запускать бинарник и проверять его вывод.
// tests/e2e_test.rs
use std::process::Command;
#[test]
fn test_cli_add() {
let output = Command::new("cargo")
.args(&["run", "--", "5", "7"])
.output()
.expect("Failed to execute command");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Result: 12"), "Expected sum 12, got: {}", stdout);
}
#[test]
fn test_cli_invalid_input() {
let output = Command::new("cargo")
.args(&["run", "--", "abc", "5"])
.output()
.expect("Failed to execute command");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("error"), "Expected error message");
}
E2E для веб-приложений
Для тестирования веб-сервисов на Rust (например, с использованием Actix-web или Axum) можно запустить сервер в фоне и отправлять запросы через reqwest:
use std::thread;
use std::time::Duration;
#[test]
fn test_api_endpoint() {
// Запускаем сервер в отдельном потоке
thread::spawn(|| {
// ваш код запуска сервера
});
thread::sleep(Duration::from_secs(2)); // ждём старта
let client = reqwest::blocking