🛡️ Por que testes são o guardrail do vibe coding
Existe uma relação direta entre cobertura de testes e a capacidade de usar IA com segurança: quanto mais testes, mais seguramente o agente pode operar autonomamente. Um agente com suite robusta pode refatorar, otimizar e adicionar features com confiança — qualquer regressão é detectada imediatamente, antes de chegar a produção.
📊 O Impacto Real dos Testes em Projetos de Vibe Coding
Andrej Karpathy, 2024
"The agent needs tests the same way a pilot needs instruments. You wouldn't fly blind — you wouldn't run agentic code without a test suite that can tell you immediately when something goes wrong."
Analogia: Testes como Rede de Segurança
Imagine um acrobata (o agente) trabalhando em um trapézio (o codebase). Sem rede de segurança, cada manobra arriscada pode ser fatal. Com a rede (suite de testes), o acrobata pode executar movimentos mais ousados e complexos — sabendo que uma queda não é catastrófica. Mais testes = movimentos mais ousados possíveis.
💡 Testes como Especificação Executável
Em vibe coding avançado, testes não são apenas verificação — são especificação. Um teste bem escrito descreve exatamente o comportamento esperado do sistema. O agente usa essa especificação para implementar corretamente e para verificar se sua implementação está certa. Testes primeiro = contratos antes do código.
🧩 Testes unitários por prompt — cobertura com edge cases
A IA é excepcionalmente boa em gerar testes unitários — especialmente para identificar e cobrir edge cases que humanos frequentemente esquecem. Com o prompt certo, você obtém uma suite completa incluindo casos extremos em minutos.
📋 Exemplo Completo: Função → Prompt → Testes Gerados
Passo 1 — A função a testar (TypeScript)
// src/utils/calculateDiscount.ts
export function calculateDiscount(
price: number,
discountPercent: number
): number {
if (price <= 0) throw new Error('Price must be positive');
if (discountPercent < 0 || discountPercent > 100)
throw new Error('Discount must be between 0 and 100');
const discounted = price * (1 - discountPercent / 100);
return Math.round(discounted * 100) / 100;
}
Passo 2 — O prompt para gerar os testes
"Gere testes unitários completos para @src/utils/calculateDiscount.ts
Requisitos:
1. Casos felizes: 10%, 50%, 100% de desconto
2. Edge cases: desconto 0, preço negativo, desconto >100, tipo errado
3. Precisão: R$100 com 33.333% de desconto deve arredondar corretamente
4. Use Vitest. Descrições em português. Organize em describe blocks."
Passo 3 — Output gerado pela IA
// src/utils/calculateDiscount.test.ts
import { describe, it, expect } from 'vitest';
import { calculateDiscount } from './calculateDiscount';
describe('calculateDiscount', () => {
describe('casos felizes', () => {
it('aplica 10% de desconto corretamente', () => {
expect(calculateDiscount(100, 10)).toBe(90);
});
it('aplica 50% de desconto corretamente', () => {
expect(calculateDiscount(200, 50)).toBe(100);
});
it('aplica 100% de desconto (produto gratuito)', () => {
expect(calculateDiscount(150, 100)).toBe(0);
});
it('arredonda para 2 casas decimais', () => {
expect(calculateDiscount(100, 33.333)).toBe(66.67);
});
});
describe('edge cases — erros esperados', () => {
it('lança erro para preço negativo', () => {
expect(() => calculateDiscount(-10, 10))
.toThrow('Price must be positive');
});
it('lança erro para preço zero', () => {
expect(() => calculateDiscount(0, 10))
.toThrow('Price must be positive');
});
it('lança erro para desconto acima de 100%', () => {
expect(() => calculateDiscount(100, 101))
.toThrow('Discount must be between 0 and 100');
});
it('lança erro para desconto negativo', () => {
expect(() => calculateDiscount(100, -5))
.toThrow('Discount must be between 0 and 100');
});
});
});
✗ Test-after (reativo)
Escrever código primeiro, testes depois. A IA tende a escrever testes que confirmam o que o código já faz — inclusive seus bugs. Cobertura alta, mas qualidade baixa.
✓ Test-first (proativo)
Definir o comportamento esperado antes. Os testes tornam-se a especificação real. A IA implementa para satisfazer os testes — e qualquer desvio é detectado na hora.
💡 Peça Edge Cases Explicitamente
Sem instrução explícita, a IA gera testes para o "happy path" apenas. Sempre peça: "inclua edge cases", "teste entradas inválidas", "teste valores extremos (zero, negativo, máximo possível)". A IA conhece os edge cases — mas precisa de permissão para incluí-los.
🔗 Testes de integração — testando fluxos completos
Testes de integração verificam que componentes funcionam corretamente quando conectados. Em APIs REST, isso significa testar endpoints reais com banco de dados real (ou banco de testes). A IA gera esses testes de forma muito eficaz quando você fornece o contrato esperado da API.
🔗 Exemplo: Teste de Integração para Endpoint de Pedidos
// src/routes/orders.integration.test.ts
import request from 'supertest';
import { app } from '../app';
import { db } from '../lib/database';
import { createTestUser, generateToken } from '../test/helpers';
describe('POST /api/orders', () => {
let testToken: string;
let testUserId: string;
beforeAll(async () => {
const user = await createTestUser({ email: 'test@example.com' });
testUserId = user.id;
testToken = generateToken(user);
});
beforeEach(async () => {
await db.orders.deleteMany({ where: { userId: testUserId } });
});
afterAll(async () => {
await db.users.delete({ where: { id: testUserId } });
await db.$disconnect();
});
it('cria pedido com produtos válidos e retorna 201', async () => {
const res = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${testToken}`)
.send({ items: [{ productId: 'prod_001', quantity: 2 }] });
expect(res.status).toBe(201);
expect(res.body).toMatchObject({
id: expect.any(String),
status: 'pending',
total: expect.any(Number),
userId: testUserId,
});
// Verifica persistência no banco
const saved = await db.orders.findUnique({ where: { id: res.body.id } });
expect(saved).not.toBeNull();
expect(saved?.userId).toBe(testUserId);
});
it('retorna 401 sem token de autenticação', async () => {
const res = await request(app)
.post('/api/orders')
.send({ items: [{ productId: 'prod_001', quantity: 1 }] });
expect(res.status).toBe(401);
});
it('retorna 422 com carrinho vazio', async () => {
const res = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${testToken}`)
.send({ items: [] });
expect(res.status).toBe(422);
expect(res.body.error).toContain('items');
});
});
📋 Prompt para Gerar Testes de Integração
"Gere testes de integração para o endpoint POST /api/orders.
@src/routes/orders.ts
@src/services/OrderService.ts
@src/lib/database.ts
Requisitos:
- Use supertest para fazer requisições HTTP reais
- Crie/destrua dados de teste em beforeAll/afterAll
- Cubra: criação com sucesso, sem auth, payload inválido,
produto inexistente, estoque insuficiente
- Verifique persistência no banco após criação bem-sucedida
- Use banco de teste (DATABASE_URL do .env.test)"
✗ Não teste em integração
- •Lógica de validação simples (pertence aos unitários)
- •Cálculos matemáticos e transformações de string
- •Fluxos de UI (pertence ao E2E)
- •Tudo usando banco de dados de produção
✓ Teste em integração
- •Endpoints HTTP com auth e persistência real
- •Fluxos que envolvem múltiplos serviços juntos
- •Integrações com filas, cache e serviços externos (mockados)
- •Contratos de API (status codes, shape do response)
🎭 Testes E2E com Playwright/Cypress gerado por IA
Testes End-to-End verificam fluxos completos do ponto de vista do usuário — clicando em botões, preenchendo formulários, navegando entre páginas. A IA pode gerar testes Playwright a partir de descrições de comportamento em linguagem natural, produzindo testes robustos em minutos.
🎭 Playwright Gerado pela IA — Fluxo de Login
Output gerado a partir do prompt abaixo
// e2e/auth/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Fluxo de Login', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('login com credenciais válidas redireciona para dashboard', async ({ page }) => {
await page.getByTestId('input-email').fill('user@example.com');
await page.getByTestId('input-password').fill('SenhaSegura123');
await page.getByTestId('btn-submit').click();
await expect(page).toHaveURL('/dashboard', { timeout: 5000 });
await expect(page.getByTestId('user-greeting')).toBeVisible();
});
test('exibe erro para credenciais inválidas', async ({ page }) => {
await page.getByTestId('input-email').fill('wrong@example.com');
await page.getByTestId('input-password').fill('senhaerrada');
await page.getByTestId('btn-submit').click();
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message'))
.toContainText('Credenciais inválidas');
await expect(page).toHaveURL('/login');
});
test('botão de submit fica desabilitado com campos vazios', async ({ page }) => {
await expect(page.getByTestId('btn-submit')).toBeDisabled();
});
test('redireciona para /dashboard se já autenticado', async ({ browser }) => {
const context = await browser.newContext({
storageState: 'e2e/auth/logged-in.json' // estado salvo
});
const page = await context.newPage();
await page.goto('/login');
await expect(page).toHaveURL('/dashboard');
});
});
📋 O Prompt Usado para Gerar Este Teste
"Gere um teste Playwright para o fluxo de login da aplicação.
Fluxo principal:
1. Usuário acessa /login
2. Preenche email e senha
3. Clica em 'Entrar'
4. É redirecionado para /dashboard
Também cubra:
- Credenciais inválidas (exibir mensagem de erro)
- Botão desabilitado com campos vazios
- Redirecionamento se já autenticado
Regras obrigatórias:
- Use getByTestId() — nunca seletores CSS ou texto
- Timeout de 5s para navegações
- Organize em describe com beforeEach
- TypeScript com tipos corretos do Playwright"
📊 Playwright vs. Cypress para Testes Gerados por IA
| Critério | Playwright | Cypress |
|---|---|---|
| Qualidade de geração por IA | Excelente — mais dados de treinamento | Boa — API bem documentada |
| Multi-browser | Chrome, Firefox, Safari, Edge | Chrome-first, experimental outros |
| Paralelismo nativo | Sim, worker threads | Requer Cypress Cloud (pago) |
| Debugging visual | Trace Viewer (bom) | Time Travel Debugger (excelente) |
| Recomendado para | Projetos novos, CI prioritário | Times com foco em DX, debugging |
🔄 TDD invertido — escrever os testes primeiro
O TDD invertido é a técnica de vibe coding mais poderosa para features críticas: você pede para a IA escrever os testes que definem o comportamento esperado antes da implementação, então pede para o agente implementar o código que faz os testes passarem. O agente sabe exatamente quando terminou.
🔄 Processo em 3 Passos
Descreva o comportamento em prompt
"Escreva testes para uma função validatePassword que:
- Aceita senhas com 8+ caracteres
- Exige ao menos uma maiúscula e uma minúscula
- Exige ao menos um número
- Rejeita espaços em branco
- Retorna { valid: boolean, errors: string[] }
NÃO implemente a função ainda. Apenas os testes."
IA gera testes que falham (red)
// password.test.ts — gerado, todos falhando inicialmente
it('aceita senha válida "MinhaSenh4"', () => {
expect(validatePassword('MinhaSenh4')).toEqual({ valid: true, errors: [] });
});
it('rejeita senha sem número', () => {
const result = validatePassword('SenhaSemNum');
expect(result.valid).toBe(false);
expect(result.errors).toContain('Deve conter ao menos um número');
});
// ... mais 8 testes cobrindo todos os requisitos
IA implementa para fazer os testes passarem (green)
"Agora implemente validatePassword em TypeScript
de forma que @src/utils/password.test.ts passe
completamente. Execute npm run test para verificar
antes de finalizar. Não modifique os testes."
Implementação Resultante (gerada pela IA)
// src/utils/password.ts — gerado para satisfazer os testes
export function validatePassword(password: string): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (password.length < 8)
errors.push('Deve ter ao menos 8 caracteres');
if (!/[A-Z]/.test(password))
errors.push('Deve conter ao menos uma letra maiúscula');
if (!/[a-z]/.test(password))
errors.push('Deve conter ao menos uma letra minúscula');
if (!/[0-9]/.test(password))
errors.push('Deve conter ao menos um número');
if (/\s/.test(password))
errors.push('Não deve conter espaços em branco');
return { valid: errors.length === 0, errors };
}
💡 Por Que Esta Abordagem Produz Código Melhor
Quando a IA implementa para satisfazer testes existentes, ela não pode "trapacear" — não pode tomar atalhos que funcionam no happy path mas quebram nos edge cases. Os testes forçam uma implementação completa e correta. O agente também tem uma DoD (Definition of Done) objetiva: todos os testes passando.
📈 Cobertura de código — métricas e como atingi-las com IA
Cobertura de código mede que percentual das linhas, branches e funções são exercitados pelos testes. Targets diferentes por camada são mais eficazes do que uma meta única para todo o projeto.
📊 Targets de Cobertura por Camada
| Camada | Target | Justificativa |
|---|---|---|
| Utilitários e funções puras | 90%+ | Fácil testar, alto impacto se quebrar |
| Services e lógica de negócio | 85%+ | Core do sistema, máxima cobertura de branches |
| Controllers / Endpoints | 70%+ | Coberto pelos testes de integração também |
| Testes E2E | Fluxos críticos | Login, checkout, cadastro — não % de linhas |
| Migrations e tipos | Excluir | Não contabilizar no relatório de cobertura |
📋 Prompt para Verificar e Melhorar Cobertura
"Execute npm run test:coverage e analise o relatório.
@src/services/OrderService.ts tem apenas 45% de cobertura.
Identifique os branches não cobertos no relatório HTML
(coverage/index.html) e gere testes adicionais para atingir 85%+.
Foco especial em:
- Catch blocks e caminhos de erro
- Condicionais de negócio (pedido cancelado, reembolso, estoque 0)
- Fluxos de edge case que o relatório mostra como uncovered (marcados em vermelho)
NÃO modifique o código de produção para aumentar cobertura."
⚙️ GitHub Action para Relatório de Cobertura
# .github/workflows/coverage.yml
name: Coverage Report
on:
pull_request:
branches: [main]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Comment coverage on PR
uses: davelosert/vitest-coverage-report-action@v2
# Adiciona comentário no PR com diff de cobertura
⚠️ A Armadilha dos 100% de Cobertura
100% de cobertura não significa zero bugs. Um teste que apenas verifica "não lança exceção" conta como cobertura total. A IA pode facilmente gerar testes que executam todo o código sem fazer nenhuma asserção útil.
Instrua sempre: "testes devem verificar o comportamento esperado com asserções explícitas — não apenas que o código executa sem erro." Cobertura de qualidade > cobertura de quantidade.
🔍 Debugging sistemático — protocolo em 4 passos
Debugging eficaz com IA requer isolamento do problema antes de enviar ao agente. O contexto mínimo viável para debugging é a habilidade mais valiosa: quanto menor e mais preciso o problema isolado, maior a probabilidade de resolução na primeira tentativa.
🔬 Protocolo de Debugging em 4 Passos
Isolar — reduza o escopo ao máximo
Identifique o arquivo e a função exatos onde o bug ocorre. Se possível, escreva um teste que reproduz o problema em isolamento. Quanto menor o código relevante, melhor.
Reproduzir minimamente — crie um caso de reprodução
Um teste que falha de forma determinística é muito mais útil do que uma descrição de problema. "Este teste falha com entrada X e espera Y mas retorna Z" é contexto preciso.
Fornecer contexto ao agente — use o template
Inclua: erro + stack trace + código relevante + ambiente + o que você já tentou + comportamento esperado vs. observado.
Verificar o fix — com teste automatizado
A correção deve fazer o teste de reprodução passar. Peça ao agente: "execute o teste de regressão para confirmar que o fix funciona e não quebrou outros testes."
📋 Template de Prompt para Debugging Eficaz
## Erro
TypeError: Cannot read properties of undefined (reading 'id')
at OrderService.createOrder (OrderService.ts:47)
at OrderController.post (OrderController.ts:23)
## Stack trace completo
[cole aqui]
## Código relevante
@src/services/OrderService.ts (linhas 40-55)
@src/controllers/OrderController.ts (linhas 18-30)
## Ambiente
- Node.js 22.5.0 / TypeScript 5.4 / Prisma 5.10
## O que já tentei
- console.log em OrderService.ts:46 — user está undefined
- userId chega corretamente na controller
## Comportamento esperado vs. observado
- Esperado: usuário autenticado sempre tem user populado
- Observado: user é undefined apenas quando req vem do webhook
## Teste de regressão que deve passar após o fix
it('cria pedido via webhook sem lançar erro', async () => {
// ...
});
✗ Prompts de debugging ineficazes
- •"Meu app está quebrado, olha o código"
- •Enviar apenas a mensagem de erro sem stack trace
- •Anexar 10 arquivos sem indicar qual é relevante
- •"Tenta consertar isso de algum jeito"
✓ Prompts de debugging eficazes
- •Erro + stack trace + 2 arquivos relevantes máximo
- •Comportamento esperado E observado claramente distintos
- •O que você já tentou (evita sugestões redundantes)
- •Teste que reproduz o problema de forma determinística
✅ Resumo do Módulo 4.5
Próximo Módulo:
4.6 — 🚢 CI/CD e Deploy Profissional: pipelines automáticos, SAST integrado, feature flags e monitoramento pós-deploy