Почему E2E-тестирование в Rust — это не роскошь, а необходимость
Вы когда-нибудь запускали Rust-приложение в production и через час получали панику от пользователей? «Всё работает идеально на моей машине!» — знакомая фраза? Проблема в том, что юнит-тесты проверяют только изолированные куски кода, а интеграционные — взаимодействие компонентов. Но настоящий ад начинается, когда весь пайплайн, от HTTP-запроса до базы данных, даёт сбой из-за неочевидной связки.
E2E тестирование Rust приложений (End-to-End) — это единственный способ гарантировать, что ваше API, CLI-утилита или веб-сервис работают как единое целое. В этой статье мы не просто поговорим о теории, а напишем реальные тесты, которые можно сразу внедрить в CI/CD. Никакой воды — только Rust и production-ready подход.
Что такое E2E-тестирование в контексте Rust?
В отличие от юнит-тестов (которые тестируют функции) и интеграционных тестов (которые тестируют модули), end-to-end тесты Rust проверяют полный цикл работы приложения: от входа до выхода. Для веб-сервиса это выглядит так:
- Запуск реального сервера (или его Docker-образа).
- Отправка HTTP-запроса через клиент (например,
reqwest). - Проверка ответа: статус, тело, заголовки.
- Валидация состояния базы данных (если есть).
Для CLI-приложений — это запуск бинарника с аргументами и проверка stdout/stderr. Для десктопных приложений — симуляция действий пользователя через WebDriver (Selenium).
Когда E2E-тесты обязательны?
- Вы разрабатываете публичное API.
- У вас микросервисная архитектура.
- Приложение работает с внешними сервисами (базы данных, очереди, S3).
- Вы хотите избежать регрессий при каждом коммите.
Инструментарий для E2E тестирования в Rust
Экосистема Rust богата, но для E2E нам нужно всего несколько ключевых библиотек:
| Библиотека | Назначение |
|---|---|
reqwest | HTTP-клиент для отправки запросов к тестируемому серверу. |
tokio | Асинхронный рантайм — основа для запуска тестов и сервера. |
wiremock | Mock-сервер для имитации внешних API. |
assert_cmd + predicates | Для тестирования CLI-приложений. |
fantoccini | WebDriver клиент (если нужно тестировать браузер). |
Мы будем использовать Rust testing framework из коробки (модуль test), но добавим асинхронность через tokio::test.
Практика: Пишем E2E-тесты для веб-сервиса на Actix-Web
Допустим, у нас есть простое Rust веб-приложение на Actix-Web с эндпоинтом /api/users, который возвращает список пользователей из PostgreSQL. Мы хотим убедиться, что при реальном запуске всё работает.
Шаг 1: Подготовка тестового окружения
Создадим отдельный файл tests/e2e.rs (Rust автоматически воспринимает файлы в папке tests/ как интеграционные тесты).
// tests/e2e.rs
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::time::sleep;
// Вспомогательная функция: запускаем сервер в фоне
async fn start_server() -> std::process::Child {
let child = Command::new("cargo")
.args(&["run", "--bin", "my_app"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("Failed to start server");
// Даём серверу время на запуск
sleep(Duration::from_secs(5)).await;
child
}
Важно: В реальном проекте используйте Docker или отдельную конфигурацию базы данных для тестов, чтобы не засорять production.
Шаг 2: Пишем асинхронный E2E-тест
Теперь сам тест, который проверяет, что API возвращает корректные данные:
use reqwest::Client;
use serde_json::Value;
#[tokio::test]
async fn test_get_users_endpoint() {
// Запускаем сервер (в реальном CI — он уже должен быть запущен)
let mut server = start_server().await;
let client = Client::new();
let response = client
.get("http://localhost:8080/api/users")
.send()
.await
.expect("Failed to send request");
// Проверяем HTTP-статус
assert_eq!(response.status(), 200, "API should return 200 OK");
// Проверяем, что тело ответа — валидный JSON
let body: Value = response
.json()
.await
.expect("Response should be valid JSON");
// Проверяем структуру данных
assert!(body.is_array(), "Response should be an array");
assert!(!body.as_array().unwrap().is_empty(), "Array should not be empty");
// Убиваем сервер после теста
server.kill().expect("Failed to stop server");
}
Этот тест делает три вещи: запускает реальный сервер, отправляет HTTP-запрос и проверяет ответ. Если у вас есть авторизация — добавьте заголовки. Если есть БД — предварительно заполните её тестовыми данными через миграции.
Шаг 3: Используем wiremock для внешних зависимостей
Часто ваше приложение вызывает сторонние API (например, платёжный шлюз). Чтобы не зависеть от их доступности, используем mock сервер Rust:
use wiremock::{Mock, MockServer, ResponseTemplate};
use wiremock::matchers::{method, path};
#[tokio::test]
async fn test_with_external_api_mock() {
// Запускаем mock-сервер
let mock_server = MockServer::start().await;
// Настраиваем ожидаемый ответ
Mock::given(method("GET"))
.and(path("/payments/status"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"status": "success"})))
.mount(&mock_server)
.await;
// Теперь передаём URL mock-сервера в наше приложение
// (через переменную окружения или аргумент)
std::env::set_var("PAYMENT_API_URL", mock_server.uri());
// ... далее запускаем приложение и тестируем
}
Это позволяет тестировать сценарии с ошибками HTTP (500, 404) и таймаутами.
E2E-тестирование CLI-приложений на Rust
Не все пишут веб-сервисы. CLI-утилиты тоже нуждаются в интеграцион