<h2>Fundamentos de Testes de Integração em APIs Rust</h2>
<p>Testes de integração são essenciais para validar que diferentes camadas da sua aplicação funcionam harmoniosamente. Em Rust, especialmente com o framework <code>axum</code>, 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.</p>
<p>O <code>axum::http</code> fornece tipos como <code>Request</code>, <code>Response</code>, <code>StatusCode</code> e <code>HeaderMap</code> 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 <code>TestClient</code> ou construindo requests manualmente. Isso torna os testes rápidos, isolados e determinísticos.</p>
<h2>Configurando o Ambiente de Testes</h2>
<h3>Dependências Necessárias</h3>
<p>Adicione ao seu <code>Cargo.toml</code>:</p>
<pre><code class="language-toml">[dev-dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.7"
tower = "0.4"
hyper = "1"
serde_json = "1"</code></pre>
<h3>Estrutura Básica</h3>
<pre><code class="language-rust">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!");
}</code></pre>
<p>Crie um módulo separado em <code>src/lib.rs</code> ou <code>src/main.rs</code> com a função <code>app()</code> que retorna seu <code>Router</code>. Isso permite que testes importem e reutilizem a mesma configuração da aplicação sem duplicação.</p>
<h2>Testando Endpoints com Request e Response</h2>
<h3>Validação de Status e Headers</h3>
<pre><code class="language-rust">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"));
}</code></pre>
<p>Método <code>.oneshot()</code> processa uma requisição sem manter uma conexão persistente — ideal para testes. Use <code>header::CONTENT_TYPE</code> para validar que a resposta contém o tipo correto. Extraia e serialize o body conforme necessário para assertions mais profundas.</p>
<h3>Testando Corpos JSON</h3>
<pre><code class="language-rust">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");
}</code></pre>
<p>Para corpos maiores, use <code>to_bytes()</code> do módulo <code>axum::body</code>. Desserialize com <code>serde_json::from_slice()</code> após converter os bytes. Esta abordagem funciona com qualquer tipo que implemente <code>Deserialize</code>.</p>
<h2>Cenários Avançados e Boas Práticas</h2>
<h3>Testando Middlewares e Estado Compartilhado</h3>
<pre><code class="language-rust">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"));
}</code></pre>
<h3>Tratamento de Erros e Respostas 4xx/5xx</h3>
<pre><code class="language-rust">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);
}</code></pre>
<p>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.</p>
<h2>Conclusão</h2>
<p>Dominar testes de integração em Rust com <code>axum::http</code> envolve três pilares: (1) <strong>construir requisições manuais</strong> com <code>Request::builder()</code> e validar responses; (2) <strong>testar estado compartilhado e middlewares</strong> para garantir que o contexto flua corretamente; (3) <strong>validar não apenas status codes, mas também headers e corpos</strong> para cobertura real de comportamento. Pratique testando seus endpoints antes mesmo de implementar — Test-Driven Development é especialmente poderoso em Rust.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://docs.rs/axum/latest/axum/body/index.html" target="_blank" rel="noopener noreferrer">Axum Documentation - Testing</a></li>
<li><a href="https://docs.rs/tokio/latest/tokio/attr.test.html" target="_blank" rel="noopener noreferrer">Tokio Runtime for Tests</a></li>
<li><a href="https://doc.rust-lang.org/book/ch11-00-testing.html" target="_blank" rel="noopener noreferrer">Rust Book - Testing</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP" target="_blank" rel="noopener noreferrer">HTTP Semantics - MDN Web Docs</a></li>
<li><a href="https://docs.rs/tower/latest/tower/trait.Service.html" target="_blank" rel="noopener noreferrer">Tower Service Trait</a></li>
</ul>