Diferentes tipos de testes respondem a perguntas diferentes. Um teste unitário diz se uma função se comporta corretamente de forma isolada. Um teste de carga diz se o servidor sobrevive a um pico de tráfego. Nenhum responde à pergunta do outro. A maioria das aplicações precisa de vários tipos trabalhando em conjunto.
Este post cobre os dez tipos de testes mais comuns, para que cada um serve e como um exemplo básico se parece em JavaScript ou Node.js.
1. Testes Unitários
Definição
Testes unitários verificam uma única função ou método de forma isolada. O objetivo é confirmar que um trecho de lógica se comporta corretamente para uma determinada entrada, sem envolver bancos de dados, serviços externos ou outros módulos.
Por que Testes Unitários Importam
Eles são rápidos — é possível executar milhares deles em segundos. Quando um falha, a falha é localizada: você sabe exatamente qual função está quebrada e em que entrada. São também os testes mais fáceis de escrever e manter, pois não exigem infraestrutura.
Exemplo em Node.js
Usando Jest, um framework de testes popular para JavaScript/TypeScript:
// mathOperations.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// mathOperations.test.js
const { add } = require('./mathOperations');
test('adds two numbers correctly', () => {
expect(add(2, 3)).toBe(5);
});
A função add é testada de forma isolada. Você adicionaria mais testes para casos de borda: números negativos, entradas de ponto flutuante e entradas que não são números.
2. Testes de Integração
Definição
Testes de integração verificam como módulos separados interagem entre si. Eles vão além de funções individuais para validar o fluxo de dados entre componentes: um handler de rota chamando um model de banco de dados, um cliente de API se comunicando com um serviço externo, ou dois módulos internos trocando dados.
Por que Testes de Integração Importam
Unidades individuais podem se comportar corretamente, mas ainda assim falhar quando combinadas, porque a interface entre elas está errada. Testes de integração capturam essas incompatibilidades antes que cheguem à produção.
Exemplo em Node.js
Considere uma aplicação Express.js simples com uma chamada ao banco de dados:
// userController.js
const express = require('express');
const router = express.Router();
const UserModel = require('./UserModel');
router.get('/users/:id', async (req, res) => {
const user = await UserModel.findById(req.params.id);
if (!user) return res.status(404).send('User not found');
res.json(user);
});
module.exports = router;
// integration.test.js
const request = require('supertest');
const express = require('express');
const userController = require('./userController');
const app = express();
app.use('/', userController);
describe('GET /users/:id', () => {
it('should return a user when valid id is provided', async () => {
// Mock database operations or use an in-memory DB
const response = await request(app).get('/users/123');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('id', '123');
});
});
Este teste confirma que a rota e o model de dados funcionam corretamente juntos. Não é um teste end-to-end completo, mas valida mais do que um teste unitário faria.
3. Testes Funcionais
Definição
Testes funcionais verificam se a aplicação faz o que deveria fazer do ponto de vista do usuário, checando o comportamento em relação aos requisitos especificados. O foco é no que o software faz, não em como ele faz internamente.
Por que Testes Funcionais Importam
Uma função pode estar internamente correta (todos os testes unitários passam) e ainda assim não satisfazer o requisito. Testes funcionais conectam a implementação à necessidade de negócio ou do usuário, capturando lacunas entre especificação e comportamento.
Exemplo em Node.js
Um fluxo de login testado no nível HTTP:
// login.test.js
const request = require('supertest');
const app = require('./app'); // Your Express.js app with auth routes
describe('User Login Functionality', () => {
it('should return a token if user credentials are valid', async () => {
const response = await request(app)
.post('/auth/login')
.send({ username: 'testUser', password: 'secret123' });
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('token');
});
it('should return 401 if credentials are invalid', async () => {
const response = await request(app)
.post('/auth/login')
.send({ username: 'testUser', password: 'wrongPwd' });
expect(response.status).toBe(401);
});
});
Esses testes confirmam que a funcionalidade de login funciona para credenciais válidas e rejeita as inválidas, correspondendo ao que os requisitos dizem que a funcionalidade deve fazer.
4. Testes End-to-End (E2E)
Definição
Testes end-to-end simulam interações reais do usuário por meio de um navegador, exercitando toda a stack desde a UI até o banco de dados e de volta. Eles validam que tudo funciona em conjunto em condições próximas à produção.
Por que Testes E2E Importam
Mudanças no backend podem quebrar fluxos do frontend de maneiras que nenhum teste unitário ou de integração captura. Testes E2E são a rede de segurança para essas regressões entre camadas. São mais lentos e mais custosos de manter, portanto funcionam melhor nos caminhos críticos do usuário.
Exemplo em Node.js
Usando Cypress:
// cypress/e2e/login.spec.js
describe('Login Flow E2E', () => {
it('logs in a user with valid credentials', () => {
cy.visit('http://localhost:3000/login');
cy.get('#username').type('testUser');
cy.get('#password').type('correctPassword');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.contains('Welcome, testUser');
});
});
Este teste conduz um navegador real pelo fluxo de login e confirma que o usuário chega ao dashboard com a mensagem de boas-vindas correta.
5. Testes de Performance
Definição
Testes de performance medem como a aplicação se comporta sob carga: tempos de resposta, throughput e uso de recursos em diferentes níveis de tráfego.
Por que Testes de Performance Importam
Uma aplicação que funciona perfeitamente com dez usuários simultâneos pode colapsar com mil. Testes de performance encontram esse ponto de ruptura antes que seus usuários o encontrem.
Exemplo em Node.js
Usando Artillery para testar um endpoint de API:
// sampleLoadTest.yml (Artillery syntax)
config:
target: "http://localhost:3000"
phases:
- duration: 30
arrivalRate: 10
scenarios:
- flow:
- get:
url: "/users"
Essa configuração executa 10 requisições por segundo contra o endpoint /users durante 30 segundos. As métricas resultantes mostram percentis de tempo de resposta, taxas de erro e throughput.
6. Testes de Segurança
Definição
Testes de segurança buscam vulnerabilidades: endpoints desprotegidos, injeção de SQL, XSS, CSRF e dependências com vulnerabilidades conhecidas.
Por que Testes de Segurança Importam
Uma única vulnerabilidade explorada pode expor dados de usuários, derrubar o serviço ou gerar multas regulatórias. Testes de segurança tornam as vulnerabilidades explícitas para que possam ser corrigidas antes que atacantes as encontrem.
Exemplo em Node.js
# Check for known vulnerabilities in the project's dependencies
npm audit
npm audit varre sua árvore de dependências em um banco de dados de CVEs conhecidos e reporta vulnerabilidades por severidade. Para testes mais completos, ferramentas como o OWASP ZAP podem automatizar a varredura ativa de aplicações em execução.
7. Testes de Aceitação
Definição
Testes de aceitação verificam se o software atende aos requisitos acordados com stakeholders ou clientes. Geralmente são escritos em linguagem legível por humanos e confirmam que a implementação corresponde ao caso de uso pretendido.
Por que Testes de Aceitação Importam
Correção técnica e aceitação pelos stakeholders não são a mesma coisa. Uma funcionalidade pode ser implementada exatamente conforme especificado e ainda assim não atender ao que realmente era necessário. Testes de aceitação criam uma definição compartilhada de "pronto" entre desenvolvedores e as pessoas que solicitaram a funcionalidade.
Exemplo em Node.js
Usando Cucumber com sintaxe Gherkin:
# login.feature
Feature: User Login
In order to access my account
As a registered user
I want to be able to log in using valid credentials
Scenario: Successful login
Given I am on the login page
When I enter valid credentials
Then I should be redirected to the dashboard
And I should see a welcome message
Cada passo é mapeado para uma definição de passo em JavaScript que executa a ação correspondente. O cenário é legível por stakeholders não técnicos e executável pelo test runner.
8. Testes de Carga e Estresse
Definição
Testes de carga verificam o sistema no pico de tráfego esperado. Testes de estresse vão além disso para encontrar onde ele quebra e como se recupera.
Por que Testes de Carga e Estresse Importam
Saber que seu sistema suporta 500 usuários simultâneos é útil. Saber o que acontece com 2000, e se ele se recupera graciosamente quando a carga cai, é mais útil para planejamento de capacidade e preparação para incidentes.
Exemplo em Node.js
Um cenário de teste de estresse com k6:
// stressTest.js
import http from 'k6/http';
import { sleep } from 'k6';
export let options = {
stages: [
{ duration: '2m', target: 100 }, // ramp up from 0 to 100 users over 2 minutes
{ duration: '5m', target: 100 }, // stay at 100 users for 5 minutes
{ duration: '2m', target: 200 }, // ramp up to 200 users
{ duration: '5m', target: 200 }, // stay at 200 users for 5 minutes
{ duration: '1m', target: 0 }, // ramp down
],
};
export default function() {
http.get('http://localhost:3000/api/resource');
sleep(1);
}
Este script aumenta gradualmente até 200 usuários simultâneos, mantém essa carga e depois a reduz. As métricas em cada etapa mostram como a performance se degrada sob carga sustentada.
9. Testes de Regressão
Definição
Testes de regressão reexecutam os testes existentes após mudanças no código para confirmar que funcionalidades que estavam funcionando anteriormente ainda funcionam.
Por que Testes de Regressão Importam
Toda mudança de código é uma regressão em potencial. Refatorações, atualizações de dependências e novas funcionalidades carregam o risco de quebrar algo que estava funcionando antes. Executar o conjunto completo de testes a cada commit torna esse risco explícito.
Exemplo em Node.js
Uma suíte de regressão é apenas os testes unitários, de integração e E2E existentes executados juntos:
// All test files combined under a "regression" script
{
"scripts": {
"test:regression": "jest --config=jest.config.js --runInBand"
}
}
Um único comando reexecuta tudo. Qualquer falha é uma regressão que precisa ser investigada antes do merge.
10. Testes Exploratórios
Definição
Testes exploratórios são testes manuais não roteirizados em que um desenvolvedor ou testador explora ativamente a aplicação para encontrar comportamentos inesperados, problemas de usabilidade ou casos de borda que os testes automatizados não anteciparam.
Por que Testes Exploratórios Importam
Testes automatizados só encontram bugs que alguém pensou em testar. Um testador sondando ativamente o sistema com curiosidade e conhecimento de domínio encontra uma classe diferente de problemas: fluxos de interação estranhos, estados de erro confusos e comportamentos que são tecnicamente corretos, mas praticamente errados.
Exemplo na Prática
Uma sessão de testes exploratórios pode parecer com: fazer login como usuários com diferentes níveis de permissão, cancelar operações no meio do caminho, atualizar a página em etapas críticas, enviar formulários com entradas incomuns. Nada disso é roteirizado. O testador registra o que encontra para triagem e acompanhamento.
Testes exploratórios funcionam melhor como complemento à automação, não como substituto. Automatize os caminhos conhecidos e explore os desconhecidos.
Construindo um Pipeline de Testes Abrangente
Reunir tudo isso em um pipeline funcional exige algumas decisões. Integre os testes ao CI para que cada commit dispare uma execução. Testes com falha bloqueiam merges. Use nomes de testes descritivos para que as falhas sejam autoexplicativas sem precisar mergulhar no código. Mantenha os arquivos de teste no mesmo repositório que o código que eles testam. Monitore a cobertura com Istanbul ou uma ferramenta similar para identificar caminhos não testados, especialmente no código da camada de serviço.
O objetivo é uma suíte em que cada tipo de teste cubra os modos de falha que os outros perdem: testes unitários para bugs de lógica, testes de integração para incompatibilidades de interface, testes E2E para regressões entre camadas, e testes de performance e segurança para riscos operacionais. Cada camada é rápida onde pode ser, e completa onde a velocidade não importa tanto.