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
[](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
- Vitest Documentation — Documentação oficial do Vitest com guias de instalação, configuração e exemplos práticos
- Jest Documentation - Getting Started — Guia oficial do Jest para iniciar testes em projetos JavaScript/TypeScript
- Testing Library - Guia de boas práticas — Princípios para escrever testes mais confiáveis e focados no comportamento do usuário
- Martin Fowler - UnitTest — Artigo clássico sobre definição e boas práticas de testes unitários
- freeCodeCamp - How to Write Unit Tests in JavaScript — Tutorial prático com exemplos de código para iniciantes em testes unitários
- GitHub - husky — Ferramenta para configurar hooks do Git, incluindo execução de testes no pre-commit
- Istanbul - Cobertura de código — Ferramenta de medição de cobertura que integra com Vitest e Jest