Testes unitários: por onde começar se seu projeto não tem nenhum

1. Por que testar agora? O custo de não testar

Existe um mito persistente no desenvolvimento de software: que escrever testes "atrasa o desenvolvimento". A realidade é oposta. Um estudo da NIST estima que corrigir um bug em produção custa 30 vezes mais do que corrigi-lo durante o desenvolvimento. Sem testes, cada alteração no código se torna uma aposta.

Considere um cenário real: você modifica uma função de formatação de data que é usada em 15 lugares diferentes do sistema. Sem testes, você precisa verificar manualmente cada tela. Com um teste unitário, você descobre em 2 segundos se algo quebrou.

Os benefícios imediatos de começar a testar incluem:
- Documentação viva: testes bem escritos mostram como o código deve se comportar
- Refatoração segura: você pode melhorar o código sem medo de quebrar funcionalidades
- Confiança na entrega: saber que as partes críticas funcionam antes de ir para produção

2. Escolhendo a primeira ferramenta de teste

Para projetos JavaScript/TypeScript, duas opções se destacam: Jest e Vitest. Ambos têm comunidades ativas e baixa curva de aprendizado.

Jest é a escolha mais consolidada, com ampla documentação e integração com Create React App e Next.js. Vitest é mais recente, mas oferece integração nativa com Vite e melhor performance em projetos que já usam esse bundler.

Configuração mínima com Vitest:

// Instalação
npm install -D vitest

// package.json
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
  }
}

// vitest.config.js
import { defineConfig } from 'vitest/config'
export default defineConfig({
  test: {
    globals: true,
    environment: 'node'
  }
})

Em menos de 10 minutos você tem um ambiente de testes funcional.

3. O primeiro teste: o que testar primeiro?

Comece pelas funções puras — aquelas que, para a mesma entrada, sempre produzem a mesma saída e não têm efeitos colaterais. Essas são as mais fáceis de testar e as que mais se beneficiam dos testes.

Exemplo prático: suponha que seu projeto tenha uma função de validação de CPF:

// utils/validacao.js
export function validarCPF(cpf) {
  const cpfLimpo = cpf.replace(/\D/g, '')
  if (cpfLimpo.length !== 11) return false
  if (/^(\d)\1{10}$/.test(cpfLimpo)) return false

  let soma = 0
  for (let i = 0; i < 9; i++) {
    soma += parseInt(cpfLimpo.charAt(i)) * (10 - i)
  }
  let resto = (soma * 10) % 11
  if (resto === 10) resto = 0
  if (resto !== parseInt(cpfLimpo.charAt(9))) return false

  soma = 0
  for (let i = 0; i < 10; i++) {
    soma += parseInt(cpfLimpo.charAt(i)) * (11 - i)
  }
  resto = (soma * 10) % 11
  if (resto === 10) resto = 0
  if (resto !== parseInt(cpfLimpo.charAt(10))) return false

  return true
}

Aplicando a estrutura AAA (Arrange, Act, Assert):

// utils/validacao.test.js
import { describe, it, expect } from 'vitest'
import { validarCPF } from './validacao'

describe('validarCPF', () => {
  it('deve retornar true para CPF válido', () => {
    // Arrange
    const cpf = '529.982.247-25'

    // Act
    const resultado = validarCPF(cpf)

    // Assert
    expect(resultado).toBe(true)
  })

  it('deve retornar false para CPF com dígitos repetidos', () => {
    const cpf = '111.111.111-11'
    const resultado = validarCPF(cpf)
    expect(resultado).toBe(false)
  })

  it('deve retornar false para CPF com formato inválido', () => {
    const cpf = '123'
    const resultado = validarCPF(cpf)
    expect(resultado).toBe(false)
  })
})

4. Lidando com dependências sem refatorar tudo

Nem todo código é puro. Funções que chamam APIs ou bancos de dados precisam de isolamento. A técnica mais simples é o mocking.

Exemplo: uma função que busca dados do usuário:

// services/usuario.js
import api from './api'

export async function buscarUsuario(id) {
  const resposta = await api.get(`/usuarios/${id}`)
  return resposta.data
}

Teste com mock:

// services/usuario.test.js
import { describe, it, expect, vi } from 'vitest'
import { buscarUsuario } from './usuario'

vi.mock('./api', () => ({
  default: {
    get: vi.fn()
  }
}))

import api from './api'

describe('buscarUsuario', () => {
  it('deve retornar dados do usuário quando a requisição é bem-sucedida', async () => {
    // Arrange
    const usuarioMock = { id: 1, nome: 'João', email: 'joao@email.com' }
    api.get.mockResolvedValue({ data: usuarioMock })

    // Act
    const resultado = await buscarUsuario(1)

    // Assert
    expect(resultado).toEqual(usuarioMock)
    expect(api.get).toHaveBeenCalledWith('/usuarios/1')
  })

  it('deve lançar erro quando a requisição falha', async () => {
    api.get.mockRejectedValue(new Error('Erro de rede'))
    await expect(buscarUsuario(1)).rejects.toThrow('Erro de rede')
  })
})

Evite mocks excessivos. Para funções que processam dados sem dependências externas, teste com dados reais controlados.

5. Cobertura incremental: a regra dos 10%

Não tente cobrir 100% do código de uma vez. A regra prática é: foque em 10% do código que causa 90% dos problemas. Identifique:

  • Funções críticas para o negócio (cálculos financeiros, validações)
  • Código que já quebrou antes (regression testing)
  • Funções com muitas ramificações condicionais

Para medir cobertura:

// Adicione ao vitest.config.js
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      include: ['src/**/*.js'],
      exclude: ['src/**/*.test.js']
    }
  }
})

// Execute
npx vitest --coverage

A cada novo bug encontrado, escreva um teste que reproduza o cenário. Isso cria uma barreira contra regressões.

6. Integração com o fluxo de trabalho existente

Para garantir que os testes sejam executados regularmente, integre-os ao fluxo de trabalho sem causar atrito:

Hooks do Git (pre-commit):

// .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx vitest run --changed --bail

Este comando executa apenas os testes relacionados aos arquivos alterados, mantendo o commit rápido.

Pipeline CI (GitHub Actions):

// .github/workflows/test.yml
name: Testes
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - run: npm ci
      - run: npx vitest run --coverage

Comunique a mudança ao time destacando os benefícios: menos bugs em produção, debugging mais rápido e código mais fácil de dar manutenção.

7. Mantendo o hábito: métricas para não abandonar

Defina metas realistas para os primeiros 3 meses:

  • Mês 1: 10% de cobertura (funções críticas)
  • Mês 2: 20% de cobertura (adição de módulos principais)
  • Mês 3: 30% de cobertura (expansão para utilitários e serviços)

Use code review como aliado, não como punição. Estabeleça uma política simples: "todo novo código deve ter testes, e código existente deve ter testes adicionados quando for modificado".

Ferramentas de feedback visual ajudam a manter o hábito:

// Adicione um badge de cobertura ao README.md
[![Coverage](https://img.shields.io/badge/coverage-30%25-yellow)](https://seusite.com/coverage)

Relatórios simples mostram a evolução:

// scripts/coverage-report.js
// Gera um resumo semanal da cobertura
const { execSync } = require('child_process')
const resultado = execSync('npx vitest run --coverage').toString()
console.log('Cobertura atual:', resultado.match(/Lines\s*:\s*([\d.]+%)/)[1])

Começar a testar um projeto legado é um processo gradual. Cada teste escrito é um investimento que reduz o custo futuro de manutenção. O importante é dar o primeiro passo — mesmo que seja testar uma única função utilitária hoje.

Referências