Тестирование маршрутов в Axum
Тестирование маршрутов - важная часть разработки веб-приложений на Axum. В этом разделе рассмотрим, как писать тесты для проверки правильной работы маршрутов и роутеров.
Содержание
- Основы тестирования маршрутов
- Создание тестовых запросов
- Проверка ответов
- Тестирование различных HTTP методов
- Тестирование параметров пути
- Тестирование вложенных маршрутов
- Интеграционное тестирование
- Лучшие практики
Основы тестирования маршрутов
Axum предоставляет инструменты для тестирования приложений без необходимости запускать HTTP-сервер. Главный из них — метод oneshot
, который позволяет отправить один запрос в ваше приложение:
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()
:
// 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();
Проверка ответов
После выполнения запроса необходимо проверить правильность ответа:
#[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 методов:
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...
}
Тестирование параметров пути
Тестирование маршрутов с параметрами пути:
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()
:
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, состояние и другие компоненты:
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. Изоляция тестов
Для каждого теста создавайте новый экземпляр маршрутизатора, чтобы избежать взаимного влияния тестов.
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. Использование тестовых фикстур
Создавайте функции-помощники для настройки тестового окружения:
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. Тестирование маршрутов с ошибками
Не забывайте тестировать случаи с ошибками и некорректными запросами:
#[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. Проверка заголовков ответа
Не забывайте тестировать заголовки ответа:
#[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. Используя описанные подходы, вы сможете создавать надежные тесты, которые защитят ваше приложение от регрессионных ошибок и обеспечат корректность работы маршрутизации.