Введение в тестирование в 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