MÓDULO 4.5

🧪 Testes Automatizados com IA

Testes robustos são o guardrail que permite que agentes operem com segurança máxima. Aprenda a gerar e manter suites completas de testes usando IA — unitários, integração, E2E e TDD invertido.

7
Tópicos
35
Minutos
Avançado
Nível
Prático
Tipo
1

🛡️ 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

3x
menos falhas em produção em projetos com cobertura >80% vs. sem testes (análise de 200 projetos, GitHub 2025)
4x
mais rápido para aprovar PRs de agentes com suite robusta — reviewers confiam no pipeline, não apenas no diff
90%
dos bugs introduzidos por IA são detectados por testes antes do deploy quando cobertura >80%

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.

2

🧩 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.

3

🔗 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)
4

🎭 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
5

🔄 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

1

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."
2

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
3

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.

6

📈 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.

7

🔍 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

1

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.

2

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.

3

Fornecer contexto ao agente — use o template

Inclua: erro + stack trace + código relevante + ambiente + o que você já tentou + comportamento esperado vs. observado.

4

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

Testes = guardrail — projetos com suite robusta falham 3x menos em produção. Karpathy: "você não voa cego com agentes."
Unitários completos — função + prompt + testes gerados. Sempre peça edge cases explicitamente. Test-first produz código melhor.
Integração certa — endpoints HTTP com auth e persistência real. Não teste lógica simples em integração.
E2E com data-testid — Playwright gera testes de login/checkout por descrição natural. Seletores estáveis são essenciais.
TDD invertido — describe → testes falhando → implementação. O agente tem DoD objetiva: todos os testes passando.
Targets por camada — unitários 90%+, serviços 85%+, controllers 70%+. GitHub Action reporta diff no PR. Evite a armadilha dos 100%.
Debugging em 4 passos — isolar, reproduzir, fornecer contexto, verificar fix com teste. Template completo = resolução na primeira mensagem.

Próximo Módulo:

4.6 — 🚢 CI/CD e Deploy Profissional: pipelines automáticos, SAST integrado, feature flags e monitoramento pós-deploy