Voltar ao blog
Capa do artigo: TDD na prática com Vitest: do zero à cobertura real
TDDVitestTypeScriptFrontend

TDD na prática com Vitest: do zero à cobertura real

Como aplicar Test-Driven Development no dia a dia usando Vitest, com exemplos reais de módulos frontend complexos.

20 de abril de 20258 min de leitura

Código TypeScript no editor

Por que TDD ainda importa em 2025

Muito se fala sobre TDD como uma prática "acadêmica" que não sobrevive ao mundo real. Mas depois de aplicar consistentemente na Zenvia — em módulos como o Convert (chat multicanal) e Nutrir (automações) — posso dizer com confiança: TDD não é sobre escrever testes, é sobre design de software.

Quando você escreve o teste antes do código, você é forçado a pensar na interface pública do seu módulo antes de pensar na implementação. Isso resulta naturalmente em código mais desacoplado e testável.

Setup inicial com Vitest

pnpm add -D vitest @testing-library/react @testing-library/user-event jsdom

No vite.config.ts (ou vitest.config.ts):

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/test/setup.ts',
  },
});

O ciclo Red → Green → Refactor

Ciclo TDD Red Green Refactor

O ciclo fundamental do TDD é simples:

  1. Red: escreva um teste que falha
  2. Green: escreva o código mínimo para passar
  3. Refactor: melhore sem quebrar os testes

Exemplo: validação de formulário de contato

Começando pelo teste:

// src/validation/contactForm.test.ts
import { describe, it, expect } from 'vitest';
import { validateContactForm } from './contactForm';

describe('validateContactForm', () => {
  it('deve retornar erro quando nome está vazio', () => {
    const result = validateContactForm({ name: '', email: '[email protected]', message: 'oi' });
    expect(result.errors.name).toBe('Nome é obrigatório');
  });

  it('deve retornar sucesso quando todos campos são válidos', () => {
    const result = validateContactForm({
      name: 'Breno',
      email: '[email protected]',
      message: 'Quero contratar',
    });
    expect(result.success).toBe(true);
  });
});

Agora a implementação mínima:

// src/validation/contactForm.ts
export function validateContactForm(data: { name: string; email: string; message: string }) {
  const errors: Record<string, string> = {};

  if (!data.name.trim()) errors.name = 'Nome é obrigatório';
  if (!data.email.includes('@')) errors.email = 'E-mail inválido';
  if (!data.message.trim()) errors.message = 'Mensagem é obrigatória';

  return {
    success: Object.keys(errors).length === 0,
    errors,
  };
}

Cobertura de testes que realmente importa

Cobertura de 100% não significa nada se você está testando implementação em vez de comportamento. Foque em:

  • Casos de borda: valores vazios, nulos, extremos
  • Fluxos críticos de negócio: o que quebra se falhar?
  • Regressões: adicione testes para cada bug corrigido
pnpm vitest run --coverage

Conclusão

TDD te torna mais lento no início e muito mais rápido no médio e longo prazo. A segurança de ter uma suíte de testes sólida permite refatorações ousadas sem medo. Comece pequeno: uma função pura, um hook, uma validação. O hábito se constrói gradualmente.