Testando Serviços Web
Teste handlers do Axum sem vincular portas usando tower::ServiceExt::oneshot e fixtures de estado em memória.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
use axum::{body::Body, http::{Request, StatusCode}, Router};
use tower::ServiceExt; // oneshot
#[tokio::test]
async fn health_returns_ok() {
let app = Router::new().route("/health", axum::routing::get(|| async { "ok" }));
let response = app
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}Quando usar isso: Todo handler HTTP precisa de testes de integração rápidos sem portas de rede instáveis.
Exemplo de Trabalho
use axum::{
body::Body,
extract::State,
http::{Request, StatusCode},
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use tower::ServiceExt;
#[derive(Clone, Default)]
struct AppState { counter: std::sync::Arc<tokio::sync::Mutex<u32>> }
#[derive(Serialize)]
struct Count { value: u32 }
#[derive(Deserialize)]
struct Add { n: u32 }
async fn get_count(State(s): State<AppState>) -> Json<Count> {
Json(Count { value: *s.counter.lock().await })
}
async fn add(State(s): State<AppState>, Json(body): Json<Add>) -> StatusCode {
*s.counter.lock().await += body.n;
StatusCode::NO_CONTENT
}
fn app(state: AppState) -> Router {
Router::new()
.route("/count", get(get_count).post(add))
.with_state(state)
}
#[tokio::test]
async fn increments_counter() {
let state = AppState::default();
let mut app = app(state);
let post = Request::builder()
.method("POST")
.uri("/count")
.header("content-type", "application/json")
.body(Body::from(r#"{"n":2}"#))
.unwrap();
let res = app.oneshot(post).await.unwrap();
assert_eq!(res.status(), StatusCode::NO_CONTENT);
let get = Request::builder().uri("/count").body(Body::empty()).unwrap();
let res = app.oneshot(get).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(res.into_body(), usize::MAX).await.unwrap();
assert!(String::from_utf8_lossy(&bytes).contains(r#""value":2"#));
}O que isso demonstra:
oneshotenvia uma requisição através do serviço do roteador.with_stateinjeta fixtures de teste sem um banco de dados real.- Corpo JSON POST construído a partir de uma string bruta.
- Corpo da resposta lido com
axum::body::to_bytes.
Mergulho Profundo
Como Funciona
RouterimplementaService<Request>;oneshotevita conflitos de TCP e porta.- Cada teste constrói um
AppStatenovo para isolamento. - Para testes completos da pilha HTTP, use
axum_testoureqwestcontraTcpListenerna porta 0.
Pirâmide de Testes para APIs Web
| Nível | Ferramenta | Escopo |
|---|---|---|
| Unitário | Funções simples | Lógica de domínio |
| Handler | oneshot | Roteamento + extratores |
| E2E | reqwest + DB de teste | Pilha completa |
Notas de Rust
// Auxiliar para construir requisições JSON
fn json_request(method: &str, uri: &str, body: &str) -> Request<Body> {
Request::builder()
.method(method)
.uri(uri)
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap()
}Armadilhas
- Reutilização de app emprestado mutavelmente -
oneshotprecisa de&mut self. Correção: Crie um roteador por teste ou clone o manipulador do serviço. - Falta de
Content-Type- Extratores JSON falham sem o cabeçalho. Correção: Sempre definaapplication/jsonem testes POST. - Vazamento de estado entre testes - Estado estático compartilhado causa falhas dependentes da ordem. Correção:
AppStatenovo por teste. - Não aguardar o corpo - Descartar a resposta sem ler o corpo pode mascarar erros. Correção:
to_bytesoucollecto corpo. - Testando apenas o caminho feliz - Nenhuma asserção em 4xx/5xx. Correção: Testes orientados por tabela para casos de erro.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
Crate axum-test | Construtores de requisição de nível superior | Dependências mínimas preferidas |
reqwest contra servidor real | Casos de borda TLS ou HTTP/2 | Testes rápidos de handler tipo unitário |
mockall em traits | Isolando a camada de DB | Testando a fiação do extrator |
FAQs
Testar middleware?
Camada de middleware no roteador antes de oneshot; asserte os cabeçalhos de resposta.
Testar autenticação?
Adicione o cabeçalho Authorization; use um helper de JWT de teste.
Testes de banco de dados?
Use sqlx::test com DB efêmero ou testcontainers.
Testes de snapshot?
Crate insta para snapshots de resposta JSON - revise diffs em PRs.
Testes paralelos?
Cada teste possui portas e DB se usar TCP; testes oneshot paralelizam livremente.
Testes WebSocket?
Handshake de upgrade via reqwest ou crate de cliente dedicado.
Configuração de CI?
cargo test com o recurso de runtime multi-thread do Tokio habilitado.
Substituir tempo?
Injete o trait Clock em AppState para testes determinísticos de expiração.
Testes de propriedade?
proptest para gerar corpos JSON válidos/inválidos.
Testes de carga?
drill ou oha - separados dos testes unitários de handler.
Relacionados
- Roteamento e Handlers do Axum - Rotas sob teste
- Tratamento de Erros em Handlers - Asserte JSON de erro
- Extratores e Estado - Fixtures de estado
- Validação de Requisição - Casos de teste 422
- sqlx - Macros de teste de banco de dados
Versões da Stack: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.