Rust Admin

Testes de Integração em APIs Rust com axum::http na Prática Já leu

Fundamentos de Testes de Integração em APIs Rust Testes de integração são essenciais para validar que diferentes camadas da sua aplicação funcionam harmoniosamente. Em Rust, especialmente com o framework , você testa endpoints HTTP reais, garantindo que requisições sejam processadas corretamente do início ao fim. Diferente de testes unitários que isolam funções, testes de integração verificam fluxos completos: roteamento, middlewares, lógica de negócio e respostas. O fornece tipos como , , e que você utiliza tanto na implementação quanto nos testes. A beleza do Rust é que você pode testar sua API sem disparar um servidor real — usando ou construindo requests manualmente. Isso torna os testes rápidos, isolados e determinísticos. Configurando o Ambiente de Testes Dependências Necessárias Adicione ao seu : Estrutura Básica Crie um módulo separado em ou com a função que retorna seu . Isso permite que testes importem e reutilizem a mesma configuração da aplicação sem duplicação. Testando Endpoints com Request e Response Validação de Status

Fundamentos de Testes de Integração em APIs Rust

Testes de integração são essenciais para validar que diferentes camadas da sua aplicação funcionam harmoniosamente. Em Rust, especialmente com o framework axum, você testa endpoints HTTP reais, garantindo que requisições sejam processadas corretamente do início ao fim. Diferente de testes unitários que isolam funções, testes de integração verificam fluxos completos: roteamento, middlewares, lógica de negócio e respostas.

O axum::http fornece tipos como Request, Response, StatusCode e HeaderMap que você utiliza tanto na implementação quanto nos testes. A beleza do Rust é que você pode testar sua API sem disparar um servidor real — usando TestClient ou construindo requests manualmente. Isso torna os testes rápidos, isolados e determinísticos.

Configurando o Ambiente de Testes

Dependências Necessárias

Adicione ao seu Cargo.toml:

[dev-dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.7"
tower = "0.4"
hyper = "1"
serde_json = "1"

Estrutura Básica

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

async fn hello() -> &'static str {
    "Hello, World!"
}

fn app() -> Router {
    Router::new()
        .route("/hello", get(hello))
}

#[tokio::test]
async fn test_hello_endpoint() {
    let app = app();
    let client = axum_test::TestClient::new(app);

    let response = client.get("/hello").send().await;

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().await, "Hello, World!");
}

Crie um módulo separado em src/lib.rs ou src/main.rs com a função app() que retorna seu Router. Isso permite que testes importem e reutilizem a mesma configuração da aplicação sem duplicação.

Testando Endpoints com Request e Response

Validação de Status e Headers

use axum::{
    routing::{get, post},
    Router, Json,
    http::{StatusCode, header},
};
use serde::{Deserialize, Serialize};
use tower::ServiceExt;

#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
}

async fn create_user(Json(user): Json<User>) -> (StatusCode, Json<User>) {
    (StatusCode::CREATED, Json(user))
}

async fn get_user() -> Json<User> {
    Json(User {
        id: 1,
        name: "Alice".to_string(),
    })
}

fn app() -> Router {
    Router::new()
        .route("/users", post(create_user))
        .route("/users/1", get(get_user))
}

#[tokio::test]
async fn test_create_user_success() {
    let app = app();

    let request = Request::builder()
        .method("POST")
        .uri("/users")
        .header(header::CONTENT_TYPE, "application/json")
        .body(Body::from(r#"{"id":1,"name":"Alice"}"#))
        .unwrap();

    let response = app.oneshot(request).await.unwrap();

    assert_eq!(response.status(), StatusCode::CREATED);
}

#[tokio::test]
async fn test_get_user_content_type() {
    let app = app();

    let request = Request::builder()
        .method("GET")
        .uri("/users/1")
        .body(Body::empty())
        .unwrap();

    let response = app.oneshot(request).await.unwrap();

    assert_eq!(response.status(), StatusCode::OK);
    assert!(response
        .headers()
        .get(header::CONTENT_TYPE)
        .map(|v| v.to_str().unwrap_or(""))
        .unwrap_or("")
        .contains("application/json"));
}

Método .oneshot() processa uma requisição sem manter uma conexão persistente — ideal para testes. Use header::CONTENT_TYPE para validar que a resposta contém o tipo correto. Extraia e serialize o body conforme necessário para assertions mais profundas.

Testando Corpos JSON

use axum::body::to_bytes;

#[tokio::test]
async fn test_json_response_body() {
    let app = app();

    let request = Request::builder()
        .method("GET")
        .uri("/users/1")
        .body(Body::empty())
        .unwrap();

    let response = app.oneshot(request).await.unwrap();
    let body_bytes = to_bytes(response.into_body(), usize::MAX)
        .await
        .unwrap();

    let user: User = serde_json::from_slice(&body_bytes).unwrap();

    assert_eq!(user.id, 1);
    assert_eq!(user.name, "Alice");
}

Para corpos maiores, use to_bytes() do módulo axum::body. Desserialize com serde_json::from_slice() após converter os bytes. Esta abordagem funciona com qualquer tipo que implemente Deserialize.

Cenários Avançados e Boas Práticas

Testando Middlewares e Estado Compartilhado

use axum::extract::State;
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    db_connection: Arc<String>,
}

async fn handler(State(state): State<AppState>) -> String {
    format!("Connected to: {}", state.db_connection)
}

fn app_with_state() -> Router {
    let state = AppState {
        db_connection: Arc::new("test_db".to_string()),
    };

    Router::new()
        .route("/status", get(handler))
        .with_state(state)
}

#[tokio::test]
async fn test_with_state() {
    let app = app_with_state();

    let request = Request::builder()
        .method("GET")
        .uri("/status")
        .body(Body::empty())
        .unwrap();

    let response = app.oneshot(request).await.unwrap();
    let body_bytes = to_bytes(response.into_body(), usize::MAX)
        .await
        .unwrap();

    let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
    assert!(body_str.contains("test_db"));
}

Tratamento de Erros e Respostas 4xx/5xx

use axum::http::StatusCode;

async fn forbidden_handler() -> StatusCode {
    StatusCode::FORBIDDEN
}

#[tokio::test]
async fn test_forbidden_status() {
    let app = Router::new()
        .route("/forbidden", get(forbidden_handler));

    let request = Request::builder()
        .method("GET")
        .uri("/forbidden")
        .body(Body::empty())
        .unwrap();

    let response = app.oneshot(request).await.unwrap();
    assert_eq!(response.status(), StatusCode::FORBIDDEN);
}

Validar códigos de erro é tão importante quanto validar sucesso. Seus clientes dependem de status codes corretos para lógica de retry e tratamento de erros.

Conclusão

Dominar testes de integração em Rust com axum::http envolve três pilares: (1) construir requisições manuais com Request::builder() e validar responses; (2) testar estado compartilhado e middlewares para garantir que o contexto flua corretamente; (3) validar não apenas status codes, mas também headers e corpos para cobertura real de comportamento. Pratique testando seus endpoints antes mesmo de implementar — Test-Driven Development é especialmente poderoso em Rust.

Referências


Artigos relacionados