E2E-тестирование на Rust: Полный гайд для реальных проектов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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 нам нужно всего несколько ключевых библиотек:

БиблиотекаНазначение
reqwestHTTP-клиент для отправки запросов к тестируемому серверу.
tokioАсинхронный рантайм — основа для запуска тестов и сервера.
wiremockMock-сервер для имитации внешних API.
assert_cmd + predicatesДля тестирования CLI-приложений.
fantocciniWebDriver клиент (если нужно тестировать браузер).

Мы будем использовать 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-утилиты тоже нуждаются в интеграцион

Блоги

Рекомендаций по книгам