Rust

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

8 min de leitura

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

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

<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 = &quot;1&quot;, features = [&quot;full&quot;] }

axum = &quot;0.7&quot;

tower = &quot;0.4&quot;

hyper = &quot;1&quot;

serde_json = &quot;1&quot;</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() -&gt; &amp;&#039;static str {

&quot;Hello, World!&quot;

}

fn app() -&gt; Router {

Router::new()

.route(&quot;/hello&quot;, get(hello))

}

#[tokio::test]

async fn test_hello_endpoint() {

let app = app();

let client = axum_test::TestClient::new(app);

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

assert_eq!(response.status(), StatusCode::OK);

assert_eq!(response.text().await, &quot;Hello, World!&quot;);

}</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&lt;User&gt;) -&gt; (StatusCode, Json&lt;User&gt;) {

(StatusCode::CREATED, Json(user))

}

async fn get_user() -&gt; Json&lt;User&gt; {

Json(User {

id: 1,

name: &quot;Alice&quot;.to_string(),

})

}

fn app() -&gt; Router {

Router::new()

.route(&quot;/users&quot;, post(create_user))

.route(&quot;/users/1&quot;, get(get_user))

}

#[tokio::test]

async fn test_create_user_success() {

let app = app();

let request = Request::builder()

.method(&quot;POST&quot;)

.uri(&quot;/users&quot;)

.header(header::CONTENT_TYPE, &quot;application/json&quot;)

.body(Body::from(r#&quot;{&quot;id&quot;:1,&quot;name&quot;:&quot;Alice&quot;}&quot;#))

.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(&quot;GET&quot;)

.uri(&quot;/users/1&quot;)

.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(&quot;&quot;))

.unwrap_or(&quot;&quot;)

.contains(&quot;application/json&quot;));

}</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(&quot;GET&quot;)

.uri(&quot;/users/1&quot;)

.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(&amp;body_bytes).unwrap();

assert_eq!(user.id, 1);

assert_eq!(user.name, &quot;Alice&quot;);

}</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&lt;String&gt;,

}

async fn handler(State(state): State&lt;AppState&gt;) -&gt; String {

format!(&quot;Connected to: {}&quot;, state.db_connection)

}

fn app_with_state() -&gt; Router {

let state = AppState {

db_connection: Arc::new(&quot;test_db&quot;.to_string()),

};

Router::new()

.route(&quot;/status&quot;, get(handler))

.with_state(state)

}

#[tokio::test]

async fn test_with_state() {

let app = app_with_state();

let request = Request::builder()

.method(&quot;GET&quot;)

.uri(&quot;/status&quot;)

.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(&quot;test_db&quot;));

}</code></pre>

<h3>Tratamento de Erros e Respostas 4xx/5xx</h3>

<pre><code class="language-rust">use axum::http::StatusCode;

async fn forbidden_handler() -&gt; StatusCode {

StatusCode::FORBIDDEN

}

#[tokio::test]

async fn test_forbidden_status() {

let app = Router::new()

.route(&quot;/forbidden&quot;, get(forbidden_handler));

let request = Request::builder()

.method(&quot;GET&quot;)

.uri(&quot;/forbidden&quot;)

.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>

Comentários

Mais em Rust

Dominando Slices em Rust: Referências para Partes de Coleções em Projetos Reais
Dominando Slices em Rust: Referências para Partes de Coleções em Projetos Reais

Entendendo Slices em Rust Um slice é uma referência a uma parte contígua de u...

O que Todo Dev Deve Saber sobre Introdução ao Rust: Filosofia, Instalação e Hello World
O que Todo Dev Deve Saber sobre Introdução ao Rust: Filosofia, Instalação e Hello World

A Filosofia do Rust Rust é uma linguagem de programação de sistemas que emerg...

O que Todo Dev Deve Saber sobre HashMap e HashSet em Rust: Estruturas de Dados por Chave
O que Todo Dev Deve Saber sobre HashMap e HashSet em Rust: Estruturas de Dados por Chave

HashMap: Armazenamento Eficiente com Chaves HashMap é uma estrutura de dados...