Refatoração sem testes: técnicas seguras para bases legadas
1. Por que refatorar sem testes é possível (e necessário)
Em cenários reais de desenvolvimento, a ausência de testes automatizados em bases legadas é mais regra do que exceção. Códigos que sobreviveram a décadas de manutenção, equipes que mudaram, prazos apertados e ausência de cultura de testes criam um ambiente onde esperar por cobertura completa inviabiliza qualquer melhoria. Refatorar sem testes não é ideal, mas é uma realidade técnica que exige abordagens específicas.
A diferença crucial está entre refatoração segura e reescrita arriscada. Refatoração preserva comportamento observável — a saída do sistema permanece idêntica para as mesmas entradas. Reescrita, por outro lado, introduz novas implementações que podem alterar sutilezas de comportamento. O comportamento observável, documentado por logs, arquivos de saída ou interfaces conhecidas, torna-se o substituto pragmático dos testes automatizados.
2. Preparação do terreno antes de tocar no código
Antes de qualquer alteração, é necessário mapear o terreno. Ferramentas estáticas como grep, ctags ou analisadores de dependência revelam onde uma função é chamada, quais variáveis globais afeta e quais efeitos colaterais produz. O objetivo é identificar "pontos de sutura" (seams) — locais onde podemos isolar alterações sem quebrar o resto do sistema.
Exemplo de mapeamento manual de dependências:
Função: calcularTotal(item)
Dependências:
- variável global: TAXA_IMPOSTO
- função externa: obterDesconto(cliente)
- efeito colateral: atualizaLog(usuario, "calculo realizado")
Chamada por:
- módulo: vendas/checkout.js:45
- módulo: relatorios/fatura.js:120
Crie um "cinturão de segurança" adicionando logs temporários e assertions em pontos críticos:
// Antes da refatoração
function calcularTotal(item) {
console.log("[CHECKPOINT] entrada: ", JSON.stringify(item));
let resultado = item.preco * item.quantidade;
console.log("[CHECKPOINT] saida: ", resultado);
return resultado;
}
3. Técnicas de extração e inline seguras
A técnica mais fundamental é o Extract Method, que deve preservar exatamente o comportamento original. O segredo está em extrair blocos que não dependam de variáveis locais mutáveis ou que possam ser passadas como parâmetros.
Exemplo de Extract Method seguro:
// Código original
function processarPedido(pedido) {
let total = 0;
for (let item of pedido.itens) {
total += item.preco * item.quantidade;
total += item.preco * item.quantidade * 0.1; // taxa
}
return total;
}
// Após extração
function calcularTotalItem(item) {
let base = item.preco * item.quantidade;
return base + base * 0.1;
}
function processarPedido(pedido) {
let total = 0;
for (let item of pedido.itens) {
total += calcularTotalItem(item);
}
return total;
}
Replace Temp with Query substitui variáveis temporárias por funções, eliminando dependências de estado local. Já o Inline Method deve ser usado apenas quando o método é trivial e chamado em poucos lugares — cada inline exige verificação manual de que todos os pontos de chamada mantêm o mesmo comportamento.
4. Refatoração de condicionais complexas
Condicionais aninhados são os maiores vilões em código legado. A decomposição em guard clauses e early returns simplifica drasticamente a legibilidade sem alterar comportamento:
// Código original
function calcularFrete(pedido) {
if (pedido.peso > 0) {
if (pedido.destino === "nacional") {
if (pedido.urgente) {
return pedido.peso * 10;
} else {
return pedido.peso * 5;
}
} else {
return pedido.peso * 20;
}
} else {
return 0;
}
}
// Após refatoração com guard clauses
function calcularFrete(pedido) {
if (pedido.peso <= 0) return 0;
if (pedido.destino !== "nacional") return pedido.peso * 20;
if (pedido.urgente) return pedido.peso * 10;
return pedido.peso * 5;
}
Para substituir condicionais por polimorfismo sem testes, use uma factory estática que mapeia tipos para classes concretas:
function criarProcessador(tipo) {
const mapa = {
"fisico": new ProcessadorFisico(),
"digital": new ProcessadorDigital(),
"assinatura": new ProcessadorAssinatura()
};
return mapa[tipo] || new ProcessadorPadrao();
}
Tabelas de decisão e mapas de configuração eliminam ifs aninhados substituindo lógica condicional por consultas em estruturas de dados:
const tabelaFrete = {
"nacional": { "normal": 5, "urgente": 10 },
"internacional": { "normal": 20, "urgente": 35 }
};
function calcularFrete(pedido) {
return tabelaFrete[pedido.destino]?.[pedido.tipo] || 0;
}
5. Separação de responsabilidades sem quebrar acoplamentos
Extrair classe de uma God Class requer cuidado redobrado. A abordagem segura é primeiro agrupar métodos por responsabilidade dentro da classe original, depois mover o grupo inteiro para uma nova classe, mantendo a interface original como fachada.
Exemplo de extração gradual:
// Classe original: GestorRelatorios (God Class)
class GestorRelatorios {
gerarRelatorioVendas() { /* 50 linhas */ }
gerarRelatorioClientes() { /* 40 linhas */ }
enviarEmail(relatorio) { /* 30 linhas */ }
formatarPDF(relatorio) { /* 35 linhas */ }
}
// Passo 1: Agrupar métodos de formatação
class GestorRelatorios {
constructor() {
this.formatador = new FormatadorRelatorios();
}
gerarRelatorioVendas() { /* ... */ }
enviarEmail(relatorio) { /* ... */ }
}
class FormatadorRelatorios {
formatarPDF(relatorio) { /* mesmo código original */ }
}
Introduzir parâmetro remove dependências globais, transformando variáveis implícitas em parâmetros explícitos. Isso permite testar funções isoladamente, mesmo sem testes automatizados formais.
6. Técnicas de verificação visual e comportamental
Sem testes automatizados, a verificação visual é essencial. Compare a saída antes e depois usando diff de logs ou arquivos:
# Antes da refatoração
node sistema.js > saida_antes.txt
# Depois da refatoração
node sistema.js > saida_depois.txt
# Comparação
diff saida_antes.txt saida_depois.txt
O "snapshot testing" manual com prints controlados cria um registro do comportamento esperado. Cada ponto crítico deve exibir entrada, processamento e saída. A validação por pares (pair review) foca em comportamento: o revisor não analisa estilo de código, mas verifica se a lógica permanece idêntica.
7. Estratégias de rollback e checkpoint
Cada transformação deve corresponder a um commit atômico. O padrão é: um refactor = um commit. Use branches temporárias para isolar riscos:
git checkout -b refactor/calcular-frete
# aplica a refatoração
git commit -m "refactor: substitui condicionais por tabela de decisao em calcularFrete"
# verifica comportamento
git diff main -- src/calcularFrete.js
git checkout main
git merge refactor/calcular-frete
Checklist de verificação pós-refatoração obrigatório:
- O código compila sem erros?
- O sistema roda sem exceções?
- A saída para entradas conhecidas permanece idêntica?
- Os logs de checkpoint mostram os mesmos valores?
Refatorar sem testes é uma arte de engenharia reversa disciplinada. Cada técnica apresentada substitui a segurança dos testes automatizados por protocolos manuais rigorosos. O objetivo não é eliminar testes, mas viabilizar melhorias incrementais em sistemas onde eles não existem — criando, ao longo do processo, as condições para que testes possam ser introduzidos posteriormente.
Referências
- Refactoring Guru: Extract Method — Guia completo sobre a técnica de extração de método com exemplos práticos em múltiplas linguagens.
- Martin Fowler: Refactoring (2nd Edition) — Livro referência sobre refatoração, com catálogo de técnicas e princípios de preservação de comportamento.
- Working Effectively with Legacy Code (Michael Feathers) — Clássico sobre refatoração de código legado, com técnicas de seams e caracterização de comportamento.
- Refactoring.guru: Replace Temp with Query — Tutorial específico sobre substituição de variáveis temporárias por consultas, com exemplos de código.
- SourceMaking: Refactoring Techniques — Catálogo extenso de refatorações, incluindo decomposição de condicionais e extração de classe.
- GeeksforGeeks: Refactoring Legacy Code Without Tests — Artigo técnico abordando estratégias práticas para refatorar sistemas sem cobertura de testes.
- Agile Alliance: Legacy Code Refactoring Techniques — Glossário e guia de técnicas para lidar com código legado em ambientes ágeis.