Тестирование API
Интеграционные тесты
«Тестирование API» — бесплатный урок Learn Rust Coding на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Learn Rust Coding, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Learn Rust Coding содержит 4 уроков всего.
Зачем тестировать API?
Тесты дают уверенность в том, что конечные точки работают правильно и продолжают работать при изменении кода. Для REST API наиболее полезны интеграционные тесты: они проверяют настоящий маршрутизатор от начала до конца, отправляя запросы и проверяя ответы.
В этом уроке рассматриваются модульные тесты, приём Tower с oneshot и полноценные интеграционные тесты.
Модульные тесты для чистой логики
Логику, не взаимодействующую с сетью, например проверку данных, можно тестировать обычными модульными тестами Rust. Поместите их в модуль #[cfg(test)] рядом с кодом. Они выполняются быстро с помощью cargo test.
fn validate_title(title: &str) -> bool {
!title.trim().is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_empty() {
assert!(!validate_title(" "));
assert!(validate_title("buy milk"));
}
}Асинхронные тесты
Обработчики являются асинхронными, поэтому тестовые функции с ожиданием должны выполняться в среде выполнения. Используйте #[tokio::test] вместо #[test]. Он запускает среду выполнения Tokio для этого теста и позволяет вызывать асинхронный код с помощью .await.
async fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn adds() {
assert_eq!(add(2, 3).await, 5);
}
}Тестирование без сети
Маршрутизаторы Axum реализуют трейт Service Tower, поэтому Вы можете напрямую передавать им запросы, не привязывая порт. Метод oneshot принимает один Request и возвращает Response. Благодаря этому интеграционные тесты выполняются быстро и предсказуемо.
// dev-dependencies needed: tower (for ServiceExt), http-body-util
use axum::{Router, routing::get};
use axum::http::{Request, StatusCode};
use axum::body::Body;
use tower::ServiceExt; // brings in oneshot
async fn check() {
let app = Router::new().route("/health", get(|| async { "ok" }));
let res = app
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
}Проверка кода состояния
Первое, что проверяет большинство тестов, — это статус HTTP. Для отсутствующего ресурса должен возвращаться 404, для успешного создания — 201, а для некорректного тела — 400. Создайте запрос для маршрута и сравните res.status() с ожидаемым кодом.
use axum::http::{Request, StatusCode};
use axum::body::Body;
use tower::ServiceExt;
async fn missing_returns_404(app: axum::Router) {
let res = app
.oneshot(Request::builder()
.uri("/todos/999")
.body(Body::empty()).unwrap())
.await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}Чтение тела ответа
Чтобы проверить JSON, соберите тело ответа в байты и десериализуйте его. Вспомогательный компонент http_body_util::BodyExt::collect собирает тело, затем serde_json разбирает его в вашу модель для проверки отдельных полей.
use http_body_util::BodyExt;
async fn read_json(res: axum::http::Response<axum::body::Body>) {
let bytes = res.into_body().collect().await.unwrap().to_bytes();
let todo: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(todo["done"], false);
}Отправка тела JSON
Чтобы протестировать POST, создайте запрос с телом JSON и правильным заголовком типа содержимого. Сериализуйте входную структуру, установите content-type: application/json и передайте запрос через oneshot.
use axum::http::{Request, StatusCode, header};
use axum::body::Body;
use tower::ServiceExt;
async fn create_returns_201(app: axum::Router) {
let body = serde_json::json!({ "title": "test" }).to_string();
let res = app
.oneshot(Request::builder()
.method("POST").uri("/todos")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body)).unwrap())
.await.unwrap();
assert_eq!(res.status(), StatusCode::CREATED);
}Многоразовый конструктор тестового приложения
Каждый тест должен начинаться с чистого состояния. Напишите вспомогательную функцию, которая создаёт новый маршрутизатор с новой базой данных в памяти или тестовой базой данных. Вызов этой функции в каждом тесте изолирует тесты, поэтому один тест не может повлиять на другой.
use axum::Router;
use std::sync::{Arc, Mutex};
fn test_app() -> Router {
let store = Arc::new(Mutex::new(Vec::new()));
build(store) // same build() the real server uses
}Каталог tests
Интеграционные тесты находятся в папке верхнего уровня tests/. Каждый файл в ней компилируется как отдельный пакет, использующий открытый API вашей библиотеки. Это заставляет тестировать код через тот же интерфейс, который видят реальные пользователи.
- В
tests/api.rsнаходятся тесты конечных точек. - Запустите их все с помощью
cargo test.
// tests/api.rs
use my_api::build_router; // exported from lib.rs
#[tokio::test]
async fn health_ok() {
let _app = build_router();
// send a request and assert ...
}Тестирование с базой данных
Если обработчики используют sqlx, тестам тоже нужна база данных. Распространённые стратегии: отдельная тестовая база данных, транзакции с откатом после каждого теста или макрос sqlx #[sqlx::test], который автоматически создаёт чистую базу данных для каждого теста.
// requires sqlx test features and a DATABASE_URL
use sqlx::PgPool;
#[sqlx::test]
async fn inserts_todo(pool: PgPool) {
let todo = create(&pool, "learn rust").await.unwrap();
assert_eq!(todo.title, "learn rust");
assert_eq!(todo.done, false);
}Что тестировать
Стремитесь к сбалансированному набору тестов:
- Успешный сценарий: корректные запросы возвращают правильные статус и тело.
- Ошибки: для отсутствующих ресурсов возвращается 404, для некорректных данных — 400.
- Граничные случаи: пустые списки, пограничные значения, дубликаты.
Запускайте cargo test в CI, чтобы обнаруживать регрессии до развёртывания.
Быстрая проверка
Проверьте, насколько хорошо Вы поняли тестирование API.
Итоги
Вы научились тестировать REST API на Rust:
- Тестируйте чистую логику модульными тестами; для асинхронного кода используйте
#[tokio::test]. oneshotнапрямую запускает маршрутизатор, поэтому порт не нужен.- Создавайте запросы с телами и заголовками; собирайте и разбирайте тела ответов.
- Используйте отдельный конструктор приложения для каждого теста; помещайте интеграционные тесты в
tests/. #[sqlx::test]создаёт чистые базы данных; проверяйте успешные сценарии, ошибки и граничные случаи.
Часто задаваемые вопросы
Урок «Тестирование API» бесплатный?
Да — полный текст урока «Тестирование API» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Learn Rust Coding, подпишись на CoddyKit PRO. Курс Learn Rust Coding содержит 4 уроков всего.
Чему я научусь в уроке «Тестирование API»?
Интеграционные тесты Ты практикуешь Learn Rust Coding с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Learn Rust Coding?
Предыдущий опыт не требуется. Learn Rust Coding на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Тестирование API»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Learn Rust Coding?
Да. Каждый урок Learn Rust Coding включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.