API Testing

Supertest

O Supertest executa sua aplicação Node HTTP no mesmo processo e transforma testes de API em asserções simples de status, headers e corpo — sem servidor para iniciar, sem porta para gerenciar e sem rede para mockar.

beginner14 min readUpdated 16 de set. de 2026
posts.test.ts
ts
// posts.test.ts
import request from "supertest";
import { expect, test } from "vitest";
import app from "../src/app.js";

test("GET /posts returns a list", async () => {
  const res = await request(app)
    .get("/posts")
    .expect("Content-Type", /json/)
    .expect(200);

  expect(res.body).toEqual(
    expect.arrayContaining([expect.objectContaining({ id: 1 })]),
  );
});
Construído sobre
superagent
Executa contra
Seu objeto de app
Test runners
Vitest, Jest, node:test
Rede
In-process, porta efêmera
Estilo
Asserções encadeáveis
Primeiro lançamento
2011

Por que importa

Por que o Supertest merece seu lugar

Sua app, sem servidor

Aponte o Supertest para a função da aplicação exportada e ele iniciará um servidor efêmero para o teste e o fechará em seguida. Não há porta para escolher nem processo para monitorar.

Requisições encadeáveis

O encadeamento ao estilo superagent — método, set, send, query — lê-se como a requisição HTTP que você está descrevendo, fazendo com que os testes sirvam também como documentação.

Asserções no encadeamento

expect(status), expect(header, value) e expect(body) falham com a resposta real anexada, o que encurta o ciclo entre o teste vermelho e a causa do erro.

O panorama completo

Três ideias fundamentais

Você entrega ao Supertest uma aplicação, ele constrói a requisição para você, e a resposta fica disponível para asserções como qualquer outro objeto.

O objeto da app

Importar

Exporte o request listener de app.ts e mantenha o listen() em server.ts. Os testes importam a app e nunca abrem uma porta fixa.

A requisição

Compor

get, post, set, send e query constroem uma chamada HTTP. Objetos são serializados para JSON e o Content-Type correspondente é definido automaticamente.

A resposta

Validar

status, headers, body e text são valores simples, então você faz asserções sobre eles com o mesmo expect que usa em qualquer outro lugar.

HTML5 de uma olhada

A caixa de ferramentas do Supertest

request(app)

Entregue a app ao Supertest e receba de volta um construtor de requisições encadeável.

Métodos

.get, .post, .put, .patch e .delete mapeiam-se diretamente para os verbos HTTP.

Corpos

.send({...}) serializa para JSON e define o Content-Type para você.

Auth

.set('Authorization', ...) ou .auth() anexam credenciais à requisição.

Resposta

res.status, res.headers e res.body estão prontos para asserções.

Uploads

.attach() e .field() constroem envios de formulários multipart.

Fluxo

O fluxo de um teste com Supertest

Todo teste de Supertest segue o mesmo caminho, desde a importação da app até a limpeza dos dados manipulados.

  1. 1

    Importar a app

    Importe o request listener exportado, não um servidor em execução. É o mesmo objeto que seu ponto de entrada de produção utiliza.

  2. 2

    Construir a requisição

    Chame request(app) e encadeie o método, caminho, headers, query e corpo que deseja testar.

  3. 3

    Enviar e aguardar

    Aguarde (await) o encadeamento. O Supertest inicia um servidor efêmero, despacha a requisição e resolve com a resposta.

  4. 4

    Validar status e headers

    Verifique primeiro o código de status e o Content-Type; eles detectam erros de roteamento e serialização antes das asserções de corpo.

  5. 5

    Validar o corpo

    Compare o corpo parseado com a estrutura prometida pelo contrato, usando o expect do seu test runner para qualquer coisa complexa.

  6. 6

    Limpeza

    Resete os dados que você manipulou e feche o banco de dados ou pool de conexões para que o próximo teste comece de um estado conhecido.

O guia completo

Supertest: Tudo que voce precisa saber

O que é Supertest?

Supertest é uma biblioteca de asserções HTTP para Node.js. Ela recebe um objeto de app — um app Express, uma instância Fastify, ou um request listener http simples — e permite que você faça requisições a ele através de uma API encadeável (chainable), para então validar a resposta.

Ele é construído sobre o superagent, então a parte de requisições parecerá familiar se você já utilizou essa biblioteca. O que o Supertest adiciona é a ergonomia de testes: um app pode ser passado diretamente em vez de uma URL, o ciclo de vida do servidor é gerenciado automaticamente e asserções .expect() podem ser anexadas à cadeia de chamadas.

O ponto importante a entender é que o Supertest não executa um navegador e não inicia seu servidor de produção. Ele cria um servidor HTTP de curta duração no mesmo processo, vinculado a uma porta efêmera no localhost, despacha a requisição e o encerra quando a resposta é resolvida. Seu teste interage com a stack HTTP real — códigos de status, headers, corpos, serialização — sem o custo e a instabilidade de um processo separado.

Por que testar in-process

A maior parte da dificuldade nos testes de HTTP vem do servidor, não da requisição. Um processo separado precisa de uma porta, tempo de inicialização, um health check e um teardown. Portas fixas causam conflitos em CI, e uma corrida entre listen e a primeira requisição gera falhas que parecem bugs da aplicação.

Testar in-process remove tudo isso. Não há processo para iniciar, portanto, não há verificação de prontidão (readiness check). Não há porta fixa, então arquivos de teste paralelos não conflitam entre si. Não há barreira de rede, logo, uma falha aponta para o seu handler em vez de apontar para o ambiente.

Você ainda tem o pipeline de requisição completo: middleware, roteamento, body parsing, autenticação e tratamento de erros funcionam exatamente como em produção, porque são o mesmo código. O que você abre mão são as coisas que apenas um navegador real ou uma rede real podem fornecer — execução de JavaScript, um motor de renderização e o comportamento exato de um proxy na frente do seu app.

Exporte o app, não o listener

Para que o Supertest consiga importar seu app, ele precisa ser exportável. Um erro comum é definir as rotas e chamar listen no mesmo módulo, o que inicia o servidor no momento em que o arquivo é importado e causa conflitos de porta nos seus testes.

Separe essas duas responsabilidades:

// src/app.ts
import express from "express";
import { postsRouter } from "./routes/posts.js";

export const app = express();

app.use(express.json());
app.use("/posts", postsRouter);

app.use((err, req, res, next) => {
  res.status(err.status ?? 500).json({ error: err.code ?? "internal_error" });
});
// src/server.ts
import { app } from "./app.js";

app.listen(3000, () => console.log("listening on http://localhost:3000"));

Agora src/app.ts exporta o request listener e nada mais. Os testes o importam, e o ambiente de produção o importa através de server.ts. Essa simples separação é a diferença entre uma API que você consegue testar em milissegundos e uma que você precisa dar boot.

O app do Express é, por si só, uma função com a assinatura (req, res), que é exatamente o que o http.createServer do Node espera. É por isso que request(app) funciona sem qualquer adaptador. O Fastify precisa de app.server ou de um app.ready() com await, e um handler puro do Node funciona diretamente.

Fazendo uma requisição

Uma requisição começa com request(app) e o método HTTP. Todos os métodos suportados pelo superagent estão disponíveis, e a cadeia (chain) retorna o mesmo objeto de requisição, permitindo que as chamadas sejam empilhadas.

import request from "supertest";
import app from "../src/app.js";

await request(app).get("/posts");
await request(app).post("/posts").send({ title: "Hello" });
await request(app).patch("/posts/1").send({ title: "Updated" });
await request(app).delete("/posts/1");

send serializa um objeto para JSON e define o header Content-Type automaticamente. Uma string é enviada como está, o que é útil quando você deseja testar propositalmente um corpo malformado.

Os headers são definidos com set, seja um por um ou como um objeto. Parâmetros de query ficam mais limpos através de query, que os codifica e anexa para você.

await request(app)
  .get("/posts")
  .query({ page: 2, perPage: 10 })
  .set("Accept", "application/json")
  .set({ "X-Request-Id": "test-1" });

A URL resultante é /posts?page=2&perPage=10. Se você precisar de um corpo bruto — XML, uma string simples, um payload deliberadamente quebrado — passe set("Content-Type", ...) e send a string.

Fazendo asserções na resposta

A resposta é um objeto comum com status, headers, body e text. Você pode fazer asserções nela usando o expect do seu test runner, ou pode usar o .expect() do Supertest diretamente na chain.

const res = await request(app).get("/posts").expect(200);

expect(res.headers["content-type"]).toMatch(/application\/json/);
expect(res.body).toHaveLength(3);

O .expect() do Supertest é conveniente porque, em caso de falha, ele exibe a resposta completa na mensagem de erro, o que geralmente é suficiente para entender o que deu errado sem precisar adicionar logs.

await request(app)
  .get("/posts")
  .expect("Content-Type", /json/)
  .expect(200);

O .expect() aceita um código de status, o nome e valor de um header, um corpo para igualdade profunda (deep equality), ou uma função que recebe a resposta e pode lançar um erro. A forma de função é a “válvula de escape” quando uma asserção exige lógica.

await request(app)
  .get("/posts")
  .expect((res) => {
    if (!res.body.every((p: { id: number }) => p.id > 0)) {
      throw new Error("every post must have a positive id");
    }
  });

Prefira o expect do runner para qualquer coisa além da linha de status e do content type. Ele oferece diffs melhores, suporta matchers como toMatchObject e arrayContaining, e mantém o estilo de asserção consistente com o restante da sua suíte de testes.

Combinando Supertest com seu test runner

O Supertest não é um test runner. Ele constrói requisições e pode fazer asserções nas respostas, mas não descobre testes, não fornece describe e it, não faz mock de módulos nem reporta resultados. Esse trabalho pertence ao Vitest, Jest ou node:test.

Os dois se compõem perfeitamente porque uma requisição do Supertest é “thenable”. Ao usar o await, a resposta é resolvida, portanto, um teste é apenas uma função async.

import request from "supertest";
import { beforeEach, describe, expect, it } from "vitest";
import app from "../src/app.js";

describe("POST /posts", () => {
  beforeEach(async () => {
    await resetDatabase();
  });

  it("creates a post", async () => {
    const res = await request(app)
      .post("/posts")
      .send({ title: "Hello" })
      .expect(201);

    expect(res.body.title).toBe("Hello");
  });
});

Se você esquecer de usar o await, o teste passará antes mesmo de a requisição ser enviada e a falha surgirá posteriormente como uma “unhandled rejection”. Use await em await requisição, mesmo naquelas cuja única asserção seja .expect().

Testando a autenticação

A autenticação é apenas um header ou um cookie, portanto, ambos são fáceis de testar.

Para bearer tokens, configure o header Authorization. Assine um token com o mesmo secret de teste que a aplicação utiliza, em vez de chamar o provedor de identidade real.

const token = await signTestToken({ sub: "user_1", scope: "read:posts" });

await request(app).get("/posts").expect(401);

await request(app)
  .get("/posts")
  .set("Authorization", `Bearer ${token}`)
  .expect(200);

Para session cookies, o request.agent(app) mantém um cookie jar entre as requisições, o que simula um navegador após o login.

const agent = request.agent(app);

await agent
  .post("/login")
  .send({ email: "[email protected]", password: "secret" })
  .expect(204);

await agent.get("/me").expect(200);

Quando você já possui o valor de um cookie, configure-o diretamente. Os cookies são passados como um array ou como uma string separada por ponto e vírgula.

await request(app).get("/me").set("Cookie", ["session=abc123"]).expect(200);

Teste os casos negativos com tanto cuidado quanto o “happy path”: sem token, um token expirado, um token para outro audience e um token válido sem o scope necessário. Esses quatro testes protegem você muito mais do que um único caso de sucesso.

Configurando e limpando dados

O Supertest não impõe nenhuma regra sobre o banco de dados, o que significa que um banco de dados compartilhado causará vazamento de estado entre os testes, a menos que você o resete. Existem três estratégias comuns.

Resetar antes de cada teste. Limpe (truncate) as tabelas que a suíte utiliza em beforeEach. É simples e previsível, e o custo é aceitável para suítes pequenas.

beforeEach(async () => {
  await db.query("TRUNCATE posts RESTART IDENTITY CASCADE");
  await db.query("INSERT INTO posts (id, title) VALUES (1, 'Seeded')");
});

Uma transação por teste. Se a sua aplicação e o seu teste compartilharem a mesma conexão, envolva cada teste em uma transação e faça o rollback em afterEach. Isso é rápido e deixa o banco de dados intacto, mas só funciona quando a aplicação usa o mesmo cliente, o que nem sempre acontece via HTTP.

Um banco de dados novo por arquivo de teste. Inicie um banco de dados em memória ou em container para o arquivo, execute as migrações e descarte-o ao final. Isso oferece o isolamento mais forte e é a abordagem na qual a maioria das suítes de integração se estabiliza, ao custo de um primeiro teste mais lento.

Independentemente da escolha, feche o pool de conexões em afterAll. Um pool aberto mantém o processo do Node.js ativo e transforma uma suíte que passou em um job de CI travado.

Testando os caminhos de erro (unhappy paths)

Uma rota não está testada até que suas falhas sejam testadas. O status code faz parte do contrato, portanto, faça a asserção dele explicitamente.

await request(app).get("/posts/999").expect(404);
await request(app).post("/posts").send({}).expect(422);
await request(app).get("/admin").expect(403);

Uma falha de validação deve informar qual campo falhou, e não apenas que algo deu errado.

const res = await request(app)
  .post("/posts")
  .send({ title: "" })
  .expect(422);

expect(res.body).toEqual({
  error: "validation_error",
  fields: { title: "required" },
});

As distinções são importantes. 400 é uma requisição malformada, 401 significa não autenticado, 403 significa autenticado, mas sem permissão, 404 significa que o recurso não existe, e 422 significa que o corpo foi processado, mas falhou na validação. Assertar o código errado é um bug no teste que esconde um bug na aplicação.

Upload de arquivos e multipart

O Supertest constrói requisições multipart utilizando attach para arquivos e field para os campos de formulário correspondentes. Você pode passar um buffer ou um caminho; ao passar um buffer, forneça um nome de arquivo para que o servidor receba um nome coerente.

await request(app)
  .post("/users/1/avatar")
  .field("caption", "Profile picture")
  .attach("avatar", Buffer.from("fake-image"), "avatar.png")
  .expect(201);

Teste também as rejeições: arquivo ausente, arquivo acima do limite de tamanho e tipos MIME não permitidos. Uploads são um dos lugares mais comuns onde falhas de validação costumam se esconder.

Testando paginação, filtragem e ordenação

As query strings fazem parte do contrato da API, portanto, merecem testes. query as torna legíveis, e validar o comprimento e a ordenação do corpo da resposta ajuda a capturar bugs de “off-by-one” e de valores padrão.

test("GET /posts paginates", async () => {
  await seedPosts(25);

  const page1 = await request(app)
    .get("/posts")
    .query({ page: 1, perPage: 10 })
    .expect(200);

  expect(page1.body).toHaveLength(10);
  expect(page1.body[0].id).toBe(1);

  const page3 = await request(app)
    .get("/posts")
    .query({ page: 3, perPage: 10 })
    .expect(200);

  expect(page3.body).toHaveLength(5);
});

Teste os limites, não apenas o meio: a primeira página, a última página, uma página além do fim e um perPage inválido que deveria ser limitado ou rejeitado.

await request(app)
  .get("/posts")
  .query({ page: 999 })
  .expect(200)
  .expect((res) => {
    if (res.body.length !== 0) throw new Error("expected an empty page");
  });

await request(app).get("/posts").query({ perPage: 10_000 }).expect(400);

Filtragem e ordenação são igualmente testáveis, e é onde a falta de um índice ou um ORDER BY incorreto aparece como um bug sutil.

const res = await request(app)
  .get("/posts")
  .query({ status: "published", sort: "-createdAt" })
  .expect(200);

expect(
  res.body.every((p: { status: string }) => p.status === "published"),
).toBe(true);

Redirecionamentos, cookies e outros detalhes de HTTP

Nem toda resposta é um corpo JSON. Códigos de status como 301, 302 e 304, e headers como Location, Set-Cookie e Cache-Control, são frequentemente todo o comportamento sob teste.

Por padrão, o supertest não segue redirecionamentos, que é exatamente o que você deseja quando está validando o redirecionamento em si.

const res = await request(app).get("/old-posts").expect(301);

expect(res.headers.location).toBe("/posts");

Se a cadeia de redirecionamento é o que importa, redirects(1) segue um salto e resolve com a resposta final.

await request(app).get("/old-posts").redirects(1).expect(200);

Cookies ficam visíveis em set-cookie, e o jar do agent permite validar que um login definiu os atributos corretos sem a necessidade de decodificar o valor.

const res = await request.agent(app)
  .post("/login")
  .send({ email: "[email protected]", password: "secret" })
  .expect(204);

const cookie = res.headers["set-cookie"][0];
expect(cookie).toContain("HttpOnly");
expect(cookie).toContain("SameSite=Lax");

Requisições condicionais, compressão e headers de cache valem a pena ser testados quando você depende deles, pois um proxy ou CDN alterará alegremente o comportamento se os headers estiverem incorretos.

Acelerando a suíte

Uma suíte de Supertest geralmente é rápida, mas alguns hábitos ajudam a mantê-la assim conforme ela cresce.

Reutilize setups onerosos em beforeAll e resete apenas as partes mutáveis por teste. Iniciar um container ou migrar um schema uma vez por arquivo, em vez de uma vez por teste, pode reduzir minutos de uma suíte grande.

Execute os arquivos de teste em paralelo. Vitest e Jest fazem isso por padrão e, como cada requisição do Supertest usa uma porta efêmera, não há colisões para resolver. A única coisa que você deve garantir é que os arquivos não compartilhem linhas do banco de dados.

Pule trabalhos irrelevantes em testes que apenas realizam leitura. Se uma rota precisa apenas de um usuário e um post, não faça o seed de todo o conjunto de fixtures. Fixtures menores são mais rápidas de criar e mais fáceis de analisar.

# run one file while iterating
pnpm exec vitest run test/posts.test.ts

# watch the file you are editing
pnpm exec vitest test/posts.test.ts

Finalmente, mantenha os testes unitários para a lógica pura e deixe os testes HTTP cobrirem a integração. Uma suíte que passa cada cálculo por uma requisição completa é lenta e não traz confiança extra.

Organizando a suíte de testes

Espelhe a estrutura da fonte para que um teste com falha aponte para um arquivo que você consiga encontrar. Se o app tiver src/routes/posts.ts, coloque test/posts.test.ts ao lado dele na árvore de testes.

Mantenha a configuração compartilhada em um pequeno número de helpers:

  • Um test/app.ts que constrói o app com a configuração de teste.
  • Um test/db.ts que executa as migrações, limpa (truncate) e fecha o banco de dados.
  • Um test/factories.ts com funções que criam usuários, posts e tokens.
  • Um test/tokens.ts que assina um token com o secret de teste.
// test/factories.ts
export async function createUser(overrides: Partial<User> = {}) {
  return db.user.create({
    data: { email: "[email protected]", role: "member", ...overrides },
  });
}

Factories mantêm os testes legíveis porque o valor interessante é a sobrescrita (override). Um teste que diz createUser({ role: "admin" }) comunica sua intenção de uma forma que uma parede de campos literais não consegue.

Mantendo os testes independentes

Cada teste deve passar individualmente e em qualquer ordem. Essa propriedade é o que permite que um runner execute arquivos em paralelo e evita que uma única falha desencadeie outras doze falhas enganosas.

Os inimigos da independência são os estados mutáveis compartilhados: um contador no nível do módulo, uma linha semeada que outro teste deleta, um clock mockado que nunca é restaurado, ou um banco de dados que é configurado apenas uma vez. Resete as peças das quais cada teste depende e nunca dependa de um teste anterior para ter criado algo.

Quando uma fixture for genuinamente custosa — como um banco de dados migrado ou um container em execução — crie-a apenas uma vez em beforeAll e resete as partes mutáveis em beforeEach. A distinção está entre a configuração que é apenas de leitura e a configuração que sofre alterações.

Melhores práticas

  • Exporte o app de app.ts e mantenha listen() em server.ts.
  • Use await em cada requisição; a ausência de um await é um passe silencioso.
  • Valide o status code e o content type antes do corpo da resposta.
  • Teste os caminhos negativos — 400, 401, 403, 404, 422 — e não apenas o caminho feliz.
  • Assine tokens de teste com um secret de teste em vez de chamar o provedor real.
  • Resete os dados que cada teste manipula e feche o pool em afterAll.
  • Mantenha apenas um comportamento por teste, para que uma falha identifique exatamente o que quebrou.
  • Prefira o expect do runner para asserções de corpo e .expect() para a linha de status.
  • Execute a suíte de testes contra a mesma stack de middleware utilizada em produção.

Erros comuns

  • Chamar listen() no módulo importado, fazendo com que cada arquivo de teste abra uma porta.
  • Esquecer o await, o que faz com que um teste passe antes mesmo de a requisição ser enviada.
  • Compartilhar uma linha do banco de dados entre testes e depender da ordem de execução.
  • Testar o framework — como validar que o Express faz o parse de JSON — em vez de testar o seu próprio código.
  • Validar um status 200 quando a rota deveria retornar um 404.
  • Deixar o pool do banco de dados aberto, impedindo que o processo seja encerrado.
  • Fazer mocks do banco de dados de forma tão excessiva que o teste apenas prova que o mock funciona.
  • Verificar o corpo da resposta com uma comparação de string quando um matcher estrutural seria mais claro.
  • Ignorar headers como Location, Set-Cookie e diretivas de cache.

Próximos passos

O Supertest cobre a fronteira HTTP e se integra perfeitamente a todo o ecossistema ao seu redor. Leia sobre Vitest para conhecer o runner que executará esses testes, ou Jest caso seu projeto já utilize Jest. O guia de Express explica o objeto app que você está passando para o Supertest, e a seção de REST aborda os códigos de status e a semântica que suas asserções codificam. Assim que a API estiver coberta, o guia de End-to-End Testing mostra como validar as mesmas jornadas através de um navegador real.

Na pratica

Da primeira requisição a dados reais

Quatro testes que cobrem a estrutura de uma suíte de API típica.

posts.test.ts
import request from "supertest";
import { expect, test } from "vitest";
import app from "../src/app.js";

test("GET /posts returns a list", async () => {
  const res = await request(app).get("/posts").expect(200);

  expect(res.headers["content-type"]).toMatch(/json/);
  expect(res.body).toHaveLength(3);
});

Importar a app vs iniciar um servidor

O Supertest pode testar um servidor em execução via URL, mas importar a app mantém o teste em um único processo e remove problemas de porta e timing.

Preferir
import app from "../src/app.js";
import request from "supertest";

const res = await request(app).get("/health").expect(200);
Evitar
const server = app.listen(3000);

const res = await fetch("http://localhost:3000/health");
expect(res.status).toBe(200);

server.close();
// a fixed port collides in CI and the server
// may not be ready when fetch runs

Validar o contrato vs validar internos

Teste a resposta que um cliente realmente vê. Acessar o banco de dados ou helpers privados acopla a suíte a detalhes de implementação que podem mudar.

Preferir
const res = await request(app)
  .post("/posts")
  .send({ title: "Hello" })
  .expect(201);

expect(res.body).toMatchObject({ title: "Hello" });
Evitar
await request(app)
  .post("/posts")
  .send({ title: "Hello" })
  .expect(201);

const [row] = await db.query("SELECT * FROM posts");
expect(row.title).toBe("Hello");
// breaks the moment the schema or query changes

Trade-offs

Onde o Supertest para

O Supertest é um construtor de requisições e um auxiliar de asserção, não uma estratégia de teste. Saiba o que ele deliberadamente deixa para você.

Strengths

  • Quase nada para configurar

    Se você já tem um objeto de app e um test runner, um único import é toda a instalação.

  • Rápido e hermético

    Os testes rodam no mesmo processo com uma porta efêmera, portanto não há serviço externo para iniciar nem instabilidade de rede.

  • Lê-se como a requisição

    O encadeamento espelha o HTTP, o que torna as falhas fáceis de ler e a escrita de novos testes rápida.

Trade-offs

  • Não gerencia estado

    O Supertest não possui fixtures ou transações. Manter o banco de dados limpo entre os testes é inteiramente sua responsabilidade.

  • Não detecta bugs de renderização

    Tudo abaixo da fronteira HTTP é invisível. Uma resposta 200 não diz nada sobre se a UI consegue utilizá-la.

  • Não é um navegador

    JavaScript não é executado, cookies não são impostos como em um navegador, e redirecionamentos e CORS comportam-se de forma diferente.

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender Supertest?

Nosso tutorial interativo te guia por Supertest passo a passo — com quizzes e codigo real que voce pode executar no navegador.