Rust oferece um servidor HTTP rápido e com segurança de tipo em algumas centenas de linhas. O que ele não oferece é um ciclo de feedback rápido para testar esse servidor. O ciclo de compilação é longo, cargo test executa tudo novamente em uma única alteração de trait, e a maioria dos frameworks HTTP de Rust faz você escrever um teste de integração separado para cada endpoint antes mesmo de você tê-lo chamado uma vez. Se você deseja entregar uma API e não apenas um binário, você precisa de uma ferramenta que viva fora do conjunto de ferramentas do Rust e se comunique com o servidor em execução.
Este guia detalha o fluxo de trabalho completo de teste de API Rust dentro do Apidog: apontando o Apidog para o seu servidor Axum ou Actix, construindo requisições para seus endpoints, validando JSON serializado pelo Serde, lidando com autenticação JWT, simulando endpoints para que o frontend possa avançar enquanto você finaliza o handler, e empacotando tudo como um cenário de teste de CI. Ao final, você terá um projeto Apidog reutilizável que detecta desvios de contrato antes que cargo build --release seja concluído.
Se você vem de um fluxo de trabalho Postman ou curl, você também obtém gratuitamente os recursos de design-first do Apidog: uma especificação OpenAPI gerada a partir de suas requisições salvas, URLs de mock compartilháveis e ambientes de equipe. Ignore a história de migração do Postman para uma leitura separada; esta postagem permanece focada em Rust.
TL;DR
- Execute seu servidor Rust localmente (
cargo runcontra um projeto Axum ou Actix-web), adicione a URL basehttp://localhost:3000como um ambiente Apidog e armazene quaisquer segredos como variáveis. - Crie a primeira requisição para
GET /healthz, salve-a e reutilize a autenticação e a URL base em cada handler na pasta. - Para endpoints JSON, cole sua struct Serde no editor de corpo do Apidog e afirme o formato da resposta com scripts de Testes que são executados após cada envio.
- Para rotas protegidas, crie um JWT uma vez, salve-o como
{{token}}e aplique autenticação Bearer no nível da pasta para que cada teste a herde. - Simule handlers Rust incompletos no Apidog para que sua equipe de frontend possa renderizar respostas reais antes que o handler compile corretamente.
- Salve o conjunto de trabalho como um Cenário de Teste, exporte-o e execute-o em CI com
apidog-cli. Cada PR que toca em um handler agora recebe uma verificação de contrato antes da mesclagem.
Por que testar uma API Rust fora do conjunto de ferramentas Rust
cargo test é bom. É também lento, opaco para colegas de equipe que não usam Rust e construído em torno de código em vez de HTTP. Se você deseja verificar se seu handler retorna o código de status correto, o formato JSON correto, os cabeçalhos corretos e a mensagem de erro correta quando a entrada está malformada, você escreve uma nova chamada tower::ServiceExt::oneshot para cada caso. Então você mantém esse teste conforme o handler muda. Então você o escreve novamente em JavaScript para que o frontend possa atingir um mock.
Apidog oferece uma única camada de contrato sobre o servidor em execução. A requisição existe uma vez. As asserções ficam ao lado da requisição. Colegas de equipe de frontend abrem o mesmo projeto e veem as mesmas requisições que você. Quando Serde receber um atributo #[serde(rename_all = "camelCase")] daqui a três semanas, o teste que falhará será o do Apidog, não o que vai para produção.
Três razões concretas para adicionar o Apidog a um fluxo de trabalho Rust:
- Verificações de contrato se desvinculam da compilação. O Apidog é executado contra um binário em execução. Você para de esperar o
rustcpara validar que seu endpoint ainda retorna200. - Mocks são compartilháveis. Um desenvolvedor de frontend em outro fuso horário obtém uma URL que retorna o JSON correto, não uma mensagem no Slack dizendo “o handler ainda não está pronto.”
- OpenAPI gratuitamente. O Apidog pode gerar um documento OpenAPI 3.1 a partir das requisições salvas. Você entrega isso a qualquer pessoa que queira um cliente tipado sem precisar escrever uma anotação
utoipaouaideem cada rota.
Passo 1: Adicione seu servidor Rust como um ambiente Apidog
Inicie sua API Rust. Para um projeto Axum, o boilerplate é:
use axum::{routing::get, Router};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let app = Router::new().route("/healthz", get(|| async { "ok" }));
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Abra o Apidog, crie um novo projeto, então abra o Gerenciamento de Ambiente (dropdown superior direito) e adicione um ambiente chamado Rust Local:
| Variável | Valor |
|---|---|
baseUrl |
http://localhost:3000 |
token |
deixe vazio por enquanto |
apiVersion |
v1 |
Adicione um segundo ambiente chamado Rust Staging com a URL base implantada. O Apidog define o escopo das variáveis por ambiente, então você alterna de local para staging com um clique no dropdown. Sem precisar usar 'localizar e substituir' em requisições salvas.
Passo 2: Acesse o primeiro endpoint
Crie uma pasta chamada Rust API dentro do projeto, então uma nova requisição:
- Método:
GET - URL:
{{baseUrl}}/healthz
Clique em Enviar. Se seu servidor estiver em execução, você receberá um 200 com corpo ok. Salve isso como health-check. É o teste de fumaça mais simples possível e confirma que o ambiente e a URL base funcionam antes que você escreva algo mais interessante.
Se você receber um erro de conexão recusada, seu servidor não está vinculado a 0.0.0.0 ou a porta está errada. O TcpListener::bind("127.0.0.1:3000") padrão do Rust rejeitará requisições vindas de qualquer coisa que resolva para localhost em uma interface diferente; vincule a 0.0.0.0 para desenvolvimento local para que o Apidog e os contêineres Docker possam alcançá-lo.
Passo 3: Teste requisições e respostas JSON com Serde
A forma mais comum de API Rust é um handler de JSON de entrada e saída, apoiado por uma struct Serde. Adicione uma rota POST /users:
use axum::{extract::Json, routing::post, Router};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
Json(User { id: 1, name: payload.name, email: payload.email })
}
let app = Router::new().route("/users", post(create_user));
No Apidog, crie uma requisição:
- Método:
POST - URL:
{{baseUrl}}/users - Corpo (JSON):
{
"name": "Ada Lovelace",
"email": "ada@example.com"
}
Envie. Você receberá o JSON User. Salve como create-user.
Agora abra a aba Testes e adicione asserções:
pm.test("Status is 200", () => {
pm.expect(pm.response.code).to.eql(200);
});
pm.test("Body has id, name, email", () => {
const body = pm.response.json();
pm.expect(body).to.have.property("id");
pm.expect(body.name).to.eql("Ada Lovelace");
pm.expect(body.email).to.match(/^[^@]+@[^@]+$/);
});
Na próxima vez que alguém adicionar #[serde(rename_all = "camelCase")] à struct e o formato da sua resposta mudar de user_id para userId, este teste falhará antes que a alteração seja implantada. Esse é o contrato que o Apidog oferece e que cargo test não oferece, porque cargo test executa seu código Rust contra seus tipos Rust e passaria feliz com qualquer um dos formatos.
Passo 4: Cobrir casos de rejeição do Serde
A parte interessante do tratamento de JSON em Rust é o que o Serde faz com entradas inválidas. Por padrão, o Axum retorna um 422 Unprocessable Entity sem detalhes. Construa três requisições que intencionalmente quebram o esquema:
| Requisição | Corpo | Esperado |
|---|---|---|
create-user-missing-email |
{ "name": "Ada" } |
422, corpo menciona missing field email |
create-user-extra-field |
{ "name": "Ada", "email": "a@b.c", "admin": true } |
200 se #[serde(deny_unknown_fields)] estiver ausente; 422 caso contrário |
create-user-wrong-type |
{ "name": 1, "email": "a@b.c" } |
422, menciona invalid type: integer |
Afirme cada código de status em Testes. Esta é a maneira mais barata de documentar sua política de validação real. Se você ativar deny_unknown_fields mais tarde, o segundo teste ficará vermelho e informará que o contrato público mudou.
Passo 5: Teste rotas protegidas por JWT
A maioria das APIs Rust em produção esconde handlers atrás de um middleware de autenticação. O extrator JWT de axum-extra do Axum é o padrão comum:
use axum_extra::extract::cookie::PrivateCookieJar;
use jsonwebtoken::{decode, DecodingKey, Validation};
async fn me(jar: PrivateCookieJar) -> Result<Json<User>, StatusCode> {
let token = jar.get("token").ok_or(StatusCode::UNAUTHORIZED)?;
let claims = decode::<Claims>(token.value(), &DecodingKey::from_secret(b"secret"), &Validation::default())
.map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(Json(User { id: claims.claims.sub, name: "Ada".into(), email: "ada@example.com".into() }))
}
No Apidog, você não precisa criar um JWT manualmente a cada execução de teste. Crie um Script de Pré-Requisição na pasta:
const jwt = require("jsonwebtoken");
const token = jwt.sign(
{ sub: 1, exp: Math.floor(Date.now() / 1000) + 3600 },
"secret"
);
pm.environment.set("token", token);
Abra as configurações da pasta, defina Autenticação como Bearer Token, valor {{token}}. Cada requisição na pasta agora assina e apresenta um JWT novo. Erros de token expirado desaparecem de suas execuções de teste. Para um aprofundamento sobre o lado da asserção, veja como testar autenticação JWT em APIs.
Passo 6: Teste de streaming e Server-Sent Events
Frameworks web em Rust têm streaming de primeira classe. A resposta Sse do Axum envolve um futures::Stream e emite chunks text/event-stream. O formato de comunicação é data: { ... }\n\n por quadro, terminado pelo fechamento da conexão ou por um evento done explícito.
Uma requisição que consome isso se parece com qualquer GET, mas o painel de resposta no Apidog muda para o modo de streaming quando o Content-Type é text/event-stream. Você vê cada quadro conforme ele chega, com carimbos de data/hora. Essa é a visualização que você precisa ao depurar um problema de backpressure ou um flush ausente.
O que afirmar:
- O primeiro chunk chega dentro do SLA que você anuncia. O Apidog mostra a latência por chunk no painel de streaming.
- Um nome de evento específico é disparado antes do fechamento da conexão (
event: done, por exemplo). - A duração total do stream é limitada. Um handler que esquece de sair de um
while let Some(event) = stream.next().awaitfará streaming para sempre; o Apidog mostrará isso e você pode definir um tempo limite rígido nas configurações da requisição para que o teste falhe.
Se seu endpoint usa WebSockets em vez de SSE, o Apidog possui um tipo de requisição WebSocket separado. O padrão é o mesmo: construa a conexão uma vez, salve a sequência de mensagens, afirme as respostas.
Passo 7: Simule a API Rust para desenvolvimento frontend paralelo
O frontend raramente é bloqueado pelos tempos de compilação do Rust. Ele é bloqueado por handlers que ainda não existem. Os mocks do Apidog permitem que você publique uma URL estável que retorna o contrato que você e o frontend concordaram, antes que o handler seja lançado.
Clique com o botão direito em create-user, escolha Smart Mock e habilite-o. O Apidog agora serve uma resposta User sintética em https://mock.apidog.com/m1/<projectId>/users. O corpo do mock corresponde ao seu exemplo salvo. A URL do mock aceita o mesmo formato de corpo, então o frontend pode fazer um POST contra ela como se fosse o servidor Rust real.
Para mocks dinâmicos, mude para Advanced Mock e escreva um script:
return {
id: Math.floor(Math.random() * 10000),
name: body.name,
email: body.email,
createdAt: new Date().toISOString()
};
Esse mock responde ao que o frontend enviar, com um id e timestamp gerados. Quando o handler Rust estiver pronto, o frontend retorna sua URL base para http://localhost:3000 e nada mais muda. Para mais informações sobre este padrão, a equipe também aborda a construção e teste de uma API Spring Boot e o fluxo de trabalho geral de teste de API; mesma ideia, diferentes runtimes.
Passo 8: Salve como um cenário de teste de CI
Cenários de Teste do Apidog encadeiam requisições com variáveis compartilhadas e as executam sem interface gráfica. Construa um cenário:
health-check, afirme200.create-user, afirme200, capturebody.idem uma variável.create-user-missing-email, afirme422.me(com a pré-requisição JWT), afirme200e que oidretornado corresponde aoidcapturado.- Requisição SSE, afirme que o stream é concluído em 5 segundos.
Exporte o cenário como JSON, commite-o para seu repositório em tests/apidog/ e chame-o do CI:
- name: Run API contract tests
run: |
cargo build --release
./target/release/myserver &
sleep 2
apidog-cli run tests/apidog/contract.json --env "Rust Local"
Cada PR que afeta um handler agora é executado contra um binário Rust ativo com o conjunto completo de contratos. Se uma renomeação Serde, uma mudança de código de status ou um ajuste na validação JWT quebrar o formato público, o CI o detecta antes que o botão de mesclagem fique verde.
Passo 9: Gere OpenAPI a partir das requisições salvas
Quando o conjunto de requisições estiver estável, abra o menu Exportar do Apidog e escolha OpenAPI 3.1. Você obtém um documento de especificação cobrindo cada requisição salva, com os corpos que você enviou como exemplos. Entregue isso a qualquer pessoa que esteja gerando um cliente tipado (TypeScript, Swift, Kotlin, Python) e eles receberão um contrato que corresponde ao que seu servidor Rust retorna hoje, não ao que alguém escreveu manualmente em um .yaml seis meses atrás.
Se você deseja que a especificação seja adicionada ao seu repositório Rust, execute apidog-cli export do CI e grave-a em openapi.json. O próximo cargo build não muda, mas cada consumidor de sua API obtém a verdade no disco.
FAQ
- O Apidog funciona com Axum e Actix-web? Sim. O Apidog se comunica via HTTP, não com Rust. Qualquer coisa que responda a uma requisição (Axum, Actix-web, Rocket, Warp, Poem, Loco) funciona da mesma forma. A única consideração específica do Rust é vincular a
0.0.0.0em vez de127.0.0.1para testes locais. - Como eu testo handlers que entram em pânico (panic)? Execute seu servidor com o
CatchPanicLayerdotower-httpna frente do roteador. O pânico se transforma em um500com um corpo JSON. Construa uma requisição Apidog que acione o caminho de pânico e afirme o500. Se você não encapsular os pânicos, a conexão é perdida e o Apidog relata um erro de rede, o que também é um teste de contrato válido. - Posso executar o Apidog contra um binário Rust no Docker? Sim. Aponte
baseUrlpara a porta exposta do contêiner e pronto. Se o contêiner for executado dentro do Docker Compose, dê ao seu executor do Apidog a mesma rede ou use a porta mapeada do host. - E quanto ao gRPC? O Apidog possui um tipo de requisição gRPC. Importe seus arquivos
.proto, escolha um serviço e método, preencha o payload da requisição e envie. O padrão de autenticação, ambientes e cenários de teste é idêntico ao REST. - O cenário de teste substitui o
cargo test? Não. Os testes de unidade para o seu código Rust permanecem em Rust. O Apidog testa a superfície em execução: o contrato HTTP. As duas camadas detectam bugs diferentes. Um teste de unidade detecta uma função quebrada; um teste Apidog detecta um formato de resposta incorreto, um cabeçalho CORS ausente, ou um400que se tornou um422. Você quer ambos. - O Apidog é gratuito para projetos Rust de código aberto? Sim. O cliente Apidog é gratuito para indivíduos e pequenas equipes. Cenários de teste, mocks e exportação OpenAPI fazem parte do plano gratuito. Se você mantém uma API Rust pública, você pode enviar o arquivo do projeto em seu repositório para que qualquer um que o clone obtenha o conjunto de testes.
Conclusão
APIs Rust merecem um ciclo de feedback que não dependa do compilador. Uma coleção de requisições no Apidog oferece esse ciclo: HTTP real, asserções reais, mocks reais para o frontend e um cenário de CI que é executado contra o binário ativo. Crie as requisições acima uma vez, e cada futura alteração no seu handler Axum ou Actix se tornará uma execução de teste controlada em vez de uma surpresa em tempo de execução.
Baixe o Apidog e aponte-o para o seu servidor Rust. A configuração leva menos de dez minutos. O resultado é um contrato que você controla, desacoplado do cargo, e uma equipe de frontend que para de perguntar quando o handler estará pronto.
