Skip to content

Тестирование маршрутов в Axum

Тестирование маршрутов - важная часть разработки веб-приложений на Axum. В этом разделе рассмотрим, как писать тесты для проверки правильной работы маршрутов и роутеров.

Содержание

Основы тестирования маршрутов

Axum предоставляет инструменты для тестирования приложений без необходимости запускать HTTP-сервер. Главный из них — метод oneshot, который позволяет отправить один запрос в ваше приложение:

rust
use axum::{
    routing::get,
    Router,
    http::{Request, StatusCode},
    body::Body,
};
use tower::ServiceExt;

#[tokio::test]
async fn test_hello_world() {
    // Создание маршрутизатора
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }));
    
    // Создание HTTP запроса
    let request = Request::builder()
        .uri("/")
        .body(Body::empty())
        .unwrap();
    
    // Выполнение запроса с помощью oneshot
    let response = app
        .oneshot(request)
        .await
        .unwrap();
    
    // Проверка статуса ответа
    assert_eq!(response.status(), StatusCode::OK);
    
    // Проверка тела ответа
    let body = hyper::body::to_bytes(response.into_body())
        .await
        .unwrap();
    assert_eq!(&body[..], b"Hello, World!");
}

Создание тестовых запросов

Для создания тестовых запросов используется Request::builder():

rust
// GET запрос
let get_request = Request::builder()
    .uri("/users")
    .method(http::Method::GET)
    .body(Body::empty())
    .unwrap();

// POST запрос с JSON данными
let json_data = serde_json::json!({
    "name": "Алексей",
    "email": "alex@example.com"
});

let post_request = Request::builder()
    .uri("/users")
    .method(http::Method::POST)
    .header(http::header::CONTENT_TYPE, "application/json")
    .body(Body::from(serde_json::to_string(&json_data).unwrap()))
    .unwrap();

// Запрос с query параметрами
let query_request = Request::builder()
    .uri("/users?limit=10&offset=20")
    .method(http::Method::GET)
    .body(Body::empty())
    .unwrap();

// Запрос с заголовками авторизации
let auth_request = Request::builder()
    .uri("/protected")
    .method(http::Method::GET)
    .header(http::header::AUTHORIZATION, "Bearer token123")
    .body(Body::empty())
    .unwrap();

Проверка ответов

После выполнения запроса необходимо проверить правильность ответа:

rust
#[tokio::test]
async fn test_user_api() {
    // Настройка приложения
    let app = create_test_app();
    
    // Выполнение запроса
    let response = app
        .oneshot(
            Request::builder()
                .uri("/api/users/123")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    // Проверка статуса ответа
    assert_eq!(response.status(), StatusCode::OK);
    
    // Получение и проверка тела ответа
    let body = hyper::body::to_bytes(response.into_body())
        .await
        .unwrap();
    
    // Преобразование байтов в строку
    let body_str = String::from_utf8(body.to_vec()).unwrap();
    
    // Анализ JSON
    let json: serde_json::Value = serde_json::from_str(&body_str).unwrap();
    
    // Проверка полей JSON
    assert_eq!(json["id"], 123);
    assert_eq!(json["name"], "Пользователь 123");
}

Тестирование различных HTTP методов

Пример тестирования разных HTTP методов:

rust
use axum::{
    routing::{get, post, put, delete},
    Router,
    http::{Request, StatusCode, Method},
    body::Body,
};
use tower::ServiceExt;

// Обработчики для разных методов
async fn get_handler() -> &'static str { "GET" }
async fn post_handler() -> &'static str { "POST" }
async fn put_handler() -> &'static str { "PUT" }
async fn delete_handler() -> &'static str { "DELETE" }

#[tokio::test]
async fn test_http_methods() {
    // Создание маршрутизатора
    let app = Router::new()
        .route(
            "/resource",
            get(get_handler)
                .post(post_handler)
                .put(put_handler)
                .delete(delete_handler),
        );
    
    // Тест GET
    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/resource")
                .method(Method::GET)
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::OK);
    let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
    assert_eq!(&body[..], b"GET");
    
    // Тест POST
    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/resource")
                .method(Method::POST)
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::OK);
    let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
    assert_eq!(&body[..], b"POST");
    
    // И так далее для PUT и DELETE...
}

Тестирование параметров пути

Тестирование маршрутов с параметрами пути:

rust
use axum::{
    extract::Path,
    routing::get,
    Router,
};

// Обработчик с параметром пути
async fn user_by_id(Path(id): Path<String>) -> String {
    format!("Пользователь {}", id)
}

#[tokio::test]
async fn test_path_params() {
    // Создание маршрутизатора с параметром пути
    let app = Router::new()
        .route("/users/:id", get(user_by_id));
    
    // Тестирование запроса с параметром пути
    let response = app
        .oneshot(
            Request::builder()
                .uri("/users/123")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::OK);
    
    let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
    assert_eq!(&body[..], b"Пользователь 123");
    
    // Тестирование другого значения параметра
    let response = app
        .oneshot(
            Request::builder()
                .uri("/users/abc")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
    assert_eq!(&body[..], b"Пользователь abc");
}

Тестирование вложенных маршрутов

Тестирование вложенных маршрутов с помощью nest():

rust
use axum::{
    routing::get,
    Router,
};

// Обработчики
async fn api_root() -> &'static str { "API Root" }
async fn users_list() -> &'static str { "Users List" }
async fn user_details() -> &'static str { "User Details" }

#[tokio::test]
async fn test_nested_routes() {
    // Создание вложенных маршрутов
    let users_routes = Router::new()
        .route("/", get(users_list))
        .route("/:id", get(user_details));
    
    let api_routes = Router::new()
        .route("/", get(api_root))
        .nest("/users", users_routes);
    
    let app = Router::new()
        .nest("/api", api_routes);
    
    // Тест корневого API маршрута
    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/api")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::OK);
    let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
    assert_eq!(&body[..], b"API Root");
    
    // Тест списка пользователей
    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/api/users")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::OK);
    let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
    assert_eq!(&body[..], b"Users List");
    
    // Тест деталей пользователя
    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/api/users/123")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::OK);
    let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
    assert_eq!(&body[..], b"User Details");
}

Интеграционное тестирование

Для более полного тестирования, включающего middleware, состояние и другие компоненты:

rust
use axum::{
    extract::State,
    routing::get,
    middleware,
    Router,
    http::{Request, StatusCode},
    body::Body,
};
use std::sync::{Arc, Mutex};
use tower::ServiceExt;

// Структура состояния
#[derive(Clone)]
struct AppState {
    counter: Arc<Mutex<i32>>,
}

// Middleware
async fn track_requests(
    req: Request<Body>,
    next: middleware::Next<Body>,
) -> axum::response::Response {
    println!("Запрос: {}", req.uri());
    next.run(req).await
}

// Обработчик
async fn increment_counter(
    State(state): State<AppState>,
) -> String {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    format!("Счетчик: {}", *counter)
}

#[tokio::test]
async fn test_app_with_state_and_middleware() {
    // Инициализация состояния
    let state = AppState {
        counter: Arc::new(Mutex::new(0)),
    };
    
    // Создание приложения
    let app = Router::new()
        .route("/increment", get(increment_counter))
        .layer(middleware::from_fn(track_requests))
        .with_state(state.clone());
    
    // Первый запрос
    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/increment")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::OK);
    let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
    assert_eq!(&body[..], b"Счетчик: 1");
    
    // Второй запрос
    let response = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/increment")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::OK);
    let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
    assert_eq!(&body[..], b"Счетчик: 2");
    
    // Проверка состояния напрямую
    assert_eq!(*state.counter.lock().unwrap(), 2);
}

Лучшие практики

1. Изоляция тестов

Для каждого теста создавайте новый экземпляр маршрутизатора, чтобы избежать взаимного влияния тестов.

rust
fn create_test_app() -> Router {
    // Создание нового экземпляра маршрутизатора для каждого теста
    Router::new()
        .route("/users", get(list_users))
        .route("/users/:id", get(get_user))
}

#[tokio::test]
async fn test_list_users() {
    let app = create_test_app();
    // Тест...
}

#[tokio::test]
async fn test_get_user() {
    let app = create_test_app();
    // Тест...
}

2. Использование тестовых фикстур

Создавайте функции-помощники для настройки тестового окружения:

rust
async fn setup_test_db() -> PgPool {
    // Инициализация тестовой базы данных
    let pool = PgPoolOptions::new()
        .connect("postgres://postgres:password@localhost/test_db")
        .await
        .unwrap();
    
    // Миграции и заполнение тестовыми данными
    sqlx::migrate!().run(&pool).await.unwrap();
    
    pool
}

#[tokio::test]
async fn test_user_database() {
    let db = setup_test_db().await;
    let app = Router::new()
        .route("/users", get(list_users))
        .with_state(db);
    
    // Тест...
}

3. Тестирование маршрутов с ошибками

Не забывайте тестировать случаи с ошибками и некорректными запросами:

rust
#[tokio::test]
async fn test_not_found_route() {
    let app = create_test_app();
    
    // Запрос к несуществующему маршруту
    let response = app
        .oneshot(
            Request::builder()
                .uri("/non_existent_route")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    // Проверка статуса 404
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn test_method_not_allowed() {
    let app = Router::new()
        .route("/users", get(list_users)); // Только GET
    
    // POST запрос к маршруту, который поддерживает только GET
    let response = app
        .oneshot(
            Request::builder()
                .uri("/users")
                .method(Method::POST)
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    // Проверка статуса 405 Method Not Allowed
    assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
}

4. Проверка заголовков ответа

Не забывайте тестировать заголовки ответа:

rust
#[tokio::test]
async fn test_response_headers() {
    let app = Router::new()
        .route("/json", get(|| async {
            axum::Json(serde_json::json!({"message": "Hello"}))
        }));
    
    let response = app
        .oneshot(
            Request::builder()
                .uri("/json")
                .body(Body::empty())
                .unwrap()
        )
        .await
        .unwrap();
    
    // Проверка Content-Type
    assert_eq!(
        response.headers().get(http::header::CONTENT_TYPE).unwrap(),
        "application/json"
    );
}

Эффективное тестирование маршрутов в Axum требует понимания API фреймворка и его интеграции с экосистемой Rust. Используя описанные подходы, вы сможете создавать надежные тесты, которые защитят ваше приложение от регрессионных ошибок и обеспечат корректность работы маршрутизации.