<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('express');
const app = express();
app.use(express.json());
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Nome e email são obrigatórios' });
}
// Aqui virá a lógica do banco
res.status(201).json({ id: 1, name, email });
});
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id, name: 'João Silva', email: 'joao@test.com' });
});
module.exports = app;</code></pre>
<pre><code class="language-javascript">// test/users.test.js
const request = require('supertest');
const app = require('../src/app');
describe('POST /users', () => {
it('deve criar um usuário com dados válidos', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Ana', email: 'ana@test.com' });
expect(res.status).toBe(201);
expect(res.body).toHaveProperty('id');
expect(res.body.email).toBe('ana@test.com');
});
it('deve retornar 400 quando email está faltando', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Carlos' });
expect(res.status).toBe(400);
expect(res.body.error).toBeDefined();
});
});
describe('GET /users/:id', () => {
it('deve retornar um usuário pelo ID', async () => {
const res = await request(app).get('/users/123');
expect(res.status).toBe(200);
expect(res.body.id).toBe('123');
});
});</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('sqlite3').verbose();
const path = require('path');
let db;
function initDatabase(filePath = ':memory:') {
db = new sqlite3.Database(filePath);
return new Promise((resolve, reject) => {
db.serialize(() => {
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) => {
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('express');
const { getDatabase } = require('./database');
const app = express();
app.use(express.json());
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Nome e email obrigatórios' });
}
const db = getDatabase();
db.run(
'INSERT INTO users (name, email) VALUES (?, ?)',
[name, email],
function(err) {
if (err) {
return res.status(409).json({ error: 'Email já existe' });
}
res.status(201).json({ id: this.lastID, name, email });
}
);
});
app.get('/users/:id', (req, res) => {
const db = getDatabase();
db.get('SELECT * FROM users WHERE id = ?', [req.params.id], (err, row) => {
if (err || !row) {
return res.status(404).json({ error: 'Usuário não encontrado' });
}
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('../src/database');
async function seedDatabase() {
const db = getDatabase();
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('DELETE FROM users', (err) => {
if (err) reject(err);
});
db.run(
'INSERT INTO users (id, name, email) VALUES (?, ?, ?)',
[1, 'Maria Silva', 'maria@test.com'],
(err) => {
if (err) reject(err);
}
);
db.run(
'INSERT INTO users (id, name, email) VALUES (?, ?, ?)',
[2, 'João Santos', 'joao@test.com'],
(err) => {
if (err) reject(err);
else resolve();
}
);
});
});
}
async function cleanDatabase() {
const db = getDatabase();
return new Promise((resolve, reject) => {
db.run('DELETE FROM users', (err) => {
if (err) reject(err);
else resolve();
});
});
}
module.exports = { seedDatabase, cleanDatabase };</code></pre>
<pre><code class="language-javascript">// test/integration.test.js
const request = require('supertest');
const app = require('../src/app');
const { initDatabase } = require('../src/database');
const { seedDatabase, cleanDatabase } = require('./fixtures');
describe('Testes de Integração com Banco Real', () => {
beforeAll(async () => {
await initDatabase(':memory:'); // banco em memória para testes
});
beforeEach(async () => {
await seedDatabase();
});
afterEach(async () => {
await cleanDatabase();
});
it('deve listar usuário existente', async () => {
const res = await request(app).get('/users/1');
expect(res.status).toBe(200);
expect(res.body.name).toBe('Maria Silva');
expect(res.body.email).toBe('maria@test.com');
});
it('deve retornar 404 para usuário inexistente', async () => {
const res = await request(app).get('/users/999');
expect(res.status).toBe(404);
});
it('deve criar usuário novo sem conflito', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Pedro', email: 'pedro@test.com' });
expect(res.status).toBe(201);
expect(res.body.id).toBeDefined();
});
it('deve rejeitar email duplicado', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Outro', email: 'maria@test.com' }); // 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>