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