JavaScript Avançado

Como Usar Testes de Integração em Node.js: Supertest, Banco Real e Fixtures em Produção

9 min de leitura

Como Usar Testes de Integração em Node.js: Supertest, Banco Real e Fixtures em Produção

Fundamentos de Testes de Integração em Node.js Testes de integração validam o comportamento de múltiplos componentes trabalhando juntos, diferentemente de testes unitários que isolam funções individuais. Em Node.js, especialmente com aplicações Express, você precisará testar rotas HTTP, middlewares e banco de dados em conjunto. Esse tipo de teste é crítico porque revela problemas que não aparecem em testes unitários: falhas de comunicação entre camadas, erros de transação, timeouts e inconsistências de estado. A estratégia que usaremos combina três pilares: Supertest para simular requisições HTTP, um banco de dados real (não mock) para garantir comportamento autêntico, e fixtures para preparar dados consistentes entre testes. Essa abordagem detecta problemas reais de produção e oferece confiança muito maior do que mockar tudo. Supertest: Testando Rotas HTTP Supertest é a ferramenta padrão para testar aplicações Express/Node.js. Ele permite fazer requisições HTTP sem precisar de um servidor real na porta, capturando resposta completa com status, headers e body. A instalação é simples: . Vamos a

<h2>Fundamentos de Testes de Integração em Node.js</h2>

<p>Testes de integração validam o comportamento de múltiplos componentes trabalhando juntos, diferentemente de testes unitários que isolam funções individuais. Em Node.js, especialmente com aplicações Express, você precisará testar rotas HTTP, middlewares e banco de dados em conjunto. Esse tipo de teste é crítico porque revela problemas que não aparecem em testes unitários: falhas de comunicação entre camadas, erros de transação, timeouts e inconsistências de estado.</p>

<p>A estratégia que usaremos combina três pilares: <strong>Supertest</strong> para simular requisições HTTP, um <strong>banco de dados real</strong> (não mock) para garantir comportamento autêntico, e <strong>fixtures</strong> para preparar dados consistentes entre testes. Essa abordagem detecta problemas reais de produção e oferece confiança muito maior do que mockar tudo.</p>

<h2>Supertest: Testando Rotas HTTP</h2>

<p>Supertest é a ferramenta padrão para testar aplicações Express/Node.js. Ele permite fazer requisições HTTP sem precisar de um servidor real na porta, capturando resposta completa com status, headers e body. A instalação é simples: <code>npm install --save-dev supertest</code>. Vamos a um exemplo prático real.</p>

<h3>Exemplo Básico com Express</h3>

<pre><code class="language-javascript">// src/app.js

const express = require(&#039;express&#039;);

const app = express();

app.use(express.json());

app.post(&#039;/users&#039;, (req, res) =&gt; {

const { name, email } = req.body;

if (!name || !email) {

return res.status(400).json({ error: &#039;Nome e email são obrigatórios&#039; });

}

// Aqui virá a lógica do banco

res.status(201).json({ id: 1, name, email });

});

app.get(&#039;/users/:id&#039;, (req, res) =&gt; {

res.json({ id: req.params.id, name: &#039;João Silva&#039;, email: &#039;joao@test.com&#039; });

});

module.exports = app;</code></pre>

<pre><code class="language-javascript">// test/users.test.js

const request = require(&#039;supertest&#039;);

const app = require(&#039;../src/app&#039;);

describe(&#039;POST /users&#039;, () =&gt; {

it(&#039;deve criar um usuário com dados válidos&#039;, async () =&gt; {

const res = await request(app)

.post(&#039;/users&#039;)

.send({ name: &#039;Ana&#039;, email: &#039;ana@test.com&#039; });

expect(res.status).toBe(201);

expect(res.body).toHaveProperty(&#039;id&#039;);

expect(res.body.email).toBe(&#039;ana@test.com&#039;);

});

it(&#039;deve retornar 400 quando email está faltando&#039;, async () =&gt; {

const res = await request(app)

.post(&#039;/users&#039;)

.send({ name: &#039;Carlos&#039; });

expect(res.status).toBe(400);

expect(res.body.error).toBeDefined();

});

});

describe(&#039;GET /users/:id&#039;, () =&gt; {

it(&#039;deve retornar um usuário pelo ID&#039;, async () =&gt; {

const res = await request(app).get(&#039;/users/123&#039;);

expect(res.status).toBe(200);

expect(res.body.id).toBe(&#039;123&#039;);

});

});</code></pre>

<p>Repare que usamos <code>async/await</code> — Supertest retorna Promises. Cada teste faz uma requisição, valida status e resposta. A função <code>send()</code> envia JSON, <code>expect()</code> vem do Jest (framework padrão).</p>

<h2>Banco Real vs. Mocks: A Integração Verdadeira</h2>

<p>Mockar o banco de dados é tentador, mas mascara bugs. Um banco real revela race conditions, problemas de índice, locks e constraints que nunca apareceriam com mocks. A melhor prática é usar um banco de teste <strong>separado</strong> (não o de produção).</p>

<h3>Setup com SQLite ou PostgreSQL</h3>

<p>Para desenvolvimento rápido, SQLite é ideal. Para ambientes mais robustos, PostgreSQL é preferível. Vamos usar SQLite aqui por simplicidade:</p>

<pre><code class="language-javascript">// src/database.js

const sqlite3 = require(&#039;sqlite3&#039;).verbose();

const path = require(&#039;path&#039;);

let db;

function initDatabase(filePath = &#039;:memory:&#039;) {

db = new sqlite3.Database(filePath);

return new Promise((resolve, reject) =&gt; {

db.serialize(() =&gt; {

db.run(`

CREATE TABLE IF NOT EXISTS users (

id INTEGER PRIMARY KEY AUTOINCREMENT,

name TEXT NOT NULL,

email TEXT UNIQUE NOT NULL,

created_at DATETIME DEFAULT CURRENT_TIMESTAMP

)

`, (err) =&gt; {

if (err) reject(err);

else resolve();

});

});

});

}

function getDatabase() {

return db;

}

module.exports = { initDatabase, getDatabase };</code></pre>

<pre><code class="language-javascript">// src/app.js (versão 2 com banco real)

const express = require(&#039;express&#039;);

const { getDatabase } = require(&#039;./database&#039;);

const app = express();

app.use(express.json());

app.post(&#039;/users&#039;, (req, res) =&gt; {

const { name, email } = req.body;

if (!name || !email) {

return res.status(400).json({ error: &#039;Nome e email obrigatórios&#039; });

}

const db = getDatabase();

db.run(

&#039;INSERT INTO users (name, email) VALUES (?, ?)&#039;,

[name, email],

function(err) {

if (err) {

return res.status(409).json({ error: &#039;Email já existe&#039; });

}

res.status(201).json({ id: this.lastID, name, email });

}

);

});

app.get(&#039;/users/:id&#039;, (req, res) =&gt; {

const db = getDatabase();

db.get(&#039;SELECT * FROM users WHERE id = ?&#039;, [req.params.id], (err, row) =&gt; {

if (err || !row) {

return res.status(404).json({ error: &#039;Usuário não encontrado&#039; });

}

res.json(row);

});

});

module.exports = app;</code></pre>

<h2>Fixtures: Dados Consistentes e Reutilizáveis</h2>

<p>Fixtures são conjuntos de dados predefinidos que você carrega antes de cada teste. Elas garantem que os testes rodem sempre com o mesmo estado inicial, eliminando flakiness. A estratégia é: limpar o banco, inserir dados conhecidos, executar o teste, limpar novamente.</p>

<h3>Implementação Prática</h3>

<pre><code class="language-javascript">// test/fixtures.js

const { getDatabase } = require(&#039;../src/database&#039;);

async function seedDatabase() {

const db = getDatabase();

return new Promise((resolve, reject) =&gt; {

db.serialize(() =&gt; {

db.run(&#039;DELETE FROM users&#039;, (err) =&gt; {

if (err) reject(err);

});

db.run(

&#039;INSERT INTO users (id, name, email) VALUES (?, ?, ?)&#039;,

[1, &#039;Maria Silva&#039;, &#039;maria@test.com&#039;],

(err) =&gt; {

if (err) reject(err);

}

);

db.run(

&#039;INSERT INTO users (id, name, email) VALUES (?, ?, ?)&#039;,

[2, &#039;João Santos&#039;, &#039;joao@test.com&#039;],

(err) =&gt; {

if (err) reject(err);

else resolve();

}

);

});

});

}

async function cleanDatabase() {

const db = getDatabase();

return new Promise((resolve, reject) =&gt; {

db.run(&#039;DELETE FROM users&#039;, (err) =&gt; {

if (err) reject(err);

else resolve();

});

});

}

module.exports = { seedDatabase, cleanDatabase };</code></pre>

<pre><code class="language-javascript">// test/integration.test.js

const request = require(&#039;supertest&#039;);

const app = require(&#039;../src/app&#039;);

const { initDatabase } = require(&#039;../src/database&#039;);

const { seedDatabase, cleanDatabase } = require(&#039;./fixtures&#039;);

describe(&#039;Testes de Integração com Banco Real&#039;, () =&gt; {

beforeAll(async () =&gt; {

await initDatabase(&#039;:memory:&#039;); // banco em memória para testes

});

beforeEach(async () =&gt; {

await seedDatabase();

});

afterEach(async () =&gt; {

await cleanDatabase();

});

it(&#039;deve listar usuário existente&#039;, async () =&gt; {

const res = await request(app).get(&#039;/users/1&#039;);

expect(res.status).toBe(200);

expect(res.body.name).toBe(&#039;Maria Silva&#039;);

expect(res.body.email).toBe(&#039;maria@test.com&#039;);

});

it(&#039;deve retornar 404 para usuário inexistente&#039;, async () =&gt; {

const res = await request(app).get(&#039;/users/999&#039;);

expect(res.status).toBe(404);

});

it(&#039;deve criar usuário novo sem conflito&#039;, async () =&gt; {

const res = await request(app)

.post(&#039;/users&#039;)

.send({ name: &#039;Pedro&#039;, email: &#039;pedro@test.com&#039; });

expect(res.status).toBe(201);

expect(res.body.id).toBeDefined();

});

it(&#039;deve rejeitar email duplicado&#039;, async () =&gt; {

const res = await request(app)

.post(&#039;/users&#039;)

.send({ name: &#039;Outro&#039;, email: &#039;maria@test.com&#039; }); // email já existe

expect(res.status).toBe(409);

});

});</code></pre>

<p>Os hooks <code>beforeEach</code> e <code>afterEach</code> garantem isolamento: cada teste começa limpo. Isso é fundamental para evitar testes que passam juntos mas falham isolados.</p>

<h2>Conclusão</h2>

<p>Aprendemos que <strong>testes de integração exigem um banco real</strong> para revelar problemas autênticos de aplicação. <strong>Supertest simplifica testes HTTP</strong> com sintaxe limpa e promises. <strong>Fixtures providenciam dados consistentes</strong>, eliminando flakiness e tornando testes repetíveis. A combinação desses três elementos — Supertest + banco real + fixtures — é o padrão ouro em Node.js profissional. Comece pequeno, adicione testes incrementalmente conforme sua aplicação cresce, e sempre priorize testes que refletem o comportamento real.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://github.com/visionmedia/supertest" target="_blank" rel="noopener noreferrer">Supertest - GitHub</a></li>

<li><a href="https://jestjs.io/docs/getting-started" target="_blank" rel="noopener noreferrer">Jest Documentation</a></li>

<li><a href="https://expressjs.com/en/guide/testing.html" target="_blank" rel="noopener noreferrer">Express Testing Best Practices</a></li>

<li><a href="https://github.com/mapbox/node-sqlite3" target="_blank" rel="noopener noreferrer">SQLite3 for Node.js</a></li>

<li><a href="https://www.udemy.com/course/nodejs-testing/" target="_blank" rel="noopener noreferrer">Testing Node.js Applications - Udemy</a></li>

</ul>

Comentários

Mais em JavaScript Avançado

Dominando Generators Avançados: Comunicação Bidirecional e Controle de Fluxo em Projetos Reais
Dominando Generators Avançados: Comunicação Bidirecional e Controle de Fluxo em Projetos Reais

Introdução aos Generators com Comunicação Bidirecional Generators em Python s...

Gerenciamento de Estado Avançado: Zustand, Jotai e Recoil Comparados na Prática
Gerenciamento de Estado Avançado: Zustand, Jotai e Recoil Comparados na Prática

Introdução ao Gerenciamento de Estado Moderno O gerenciamento de estado é um...

Como Usar TypeScript Compiler API: tsconfig Avançado e Project References em Produção
Como Usar TypeScript Compiler API: tsconfig Avançado e Project References em Produção

TypeScript Compiler API: tsconfig Avançado e Project References Entendendo o...