Refatoração de código legado: como mudar código antigo com segurança
Como refatorar código legado com segurança: testes de caracterização, alvos por risco, passos pequenos e reversíveis e quando partir para migração incremental.

Nesta página · 6 seções
- O que torna o código legado difícil de refatorar?
- Como adicionar testes antes de mexer em qualquer coisa?
- Por onde começar a refatorar?
- Como refatorar em passos pequenos enquanto entrega funcionalidades?
- Quando a refatoração deve dar lugar a uma migração?
- Quer uma segunda opinião sobre a sua base de código?
Principais pontos
- Refatorar significa mudar a estrutura, não o comportamento. Se o comportamento muda, é uma funcionalidade ou uma correção de bug, e deve ficar num commit separado.
- Escreva testes de caracterização primeiro. Eles registram o que o código faz hoje, mesmo quando esse comportamento parece errado.
- Comece onde as mudanças são frequentes e arriscadas, não onde o código é mais feio.
- Trabalhe em passos pequenos que possam ser mergeados e revertidos isoladamente. Branches de refatoração de vida longa são onde os projetos morrem.
- Se a própria stack é o problema (runtime sem suporte, nenhum profissional disponível para contratar, nenhum jeito de testar), pare de refatorar no lugar e parta para uma migração incremental com um chaveamento na frente de tudo.
A refatoração de código legado é segura quando você muda a estrutura do código sem mudar o que ele faz, e consegue provar isso com testes antes de mexer em qualquer coisa. A ordem é: travar o comportamento atual, escolher as partes que mais doem, mudá-las em passos pequenos e reversíveis e continuar entregando funcionalidades enquanto isso.
Dá errado quando times arrumam código que não entendem, sem rede de proteção, num branch gigante.
O que torna o código legado difícil de refatorar?
Código legado é difícil de mudar porque ninguém consegue prever o que uma mudança vai quebrar; a feiura é secundária. Nas bases de código que assumi, as mesmas coisas sempre aparecem:
- Nenhum teste, ou testes que não testam nada. Você não consegue dizer se uma mudança preservou o comportamento.
- Acoplamento escondido. Uma função que parece autocontida lê uma variável global, escreve numa tabela compartilhada ou depende da ordem das chamadas.
- Comportamento que ninguém documentou. Condições de aparência estranha muitas vezes codificam um caso real de cliente ou um incidente passado. Apagá-las como código morto é um erro clássico.
- Falta de contexto. O desenvolvedor original ou a agência foi embora, e o histórico de commits diz coisas como “fix” e “update”.
- Fluxos de dinheiro misturados com todo o resto. A lógica de cobrança, checkout ou permissões está emaranhada no código de UI ou de dados, então uma edição pequena tem um raio de impacto grande.
O primeiro objetivo não é limpar nada, e sim tornar barato verificar uma mudança: se você não consegue rodar o projeto localmente, fazer um build reproduzível e ver o resultado de um teste em minutos, resolva isso primeiro. Quando assumo uma base de código herdada, sigo sempre a mesma ordem: acessos primeiro, depois o build, depois os fluxos de dinheiro, depois os riscos. A refatoração começa quando os dois primeiros estão sólidos.
Como adicionar testes antes de mexer em qualquer coisa?
Você adiciona testes de caracterização: testes que capturam o que o código realmente faz agora, não o que ele deveria fazer. Michael Feathers popularizou essa abordagem em Working Effectively with Legacy Code.
Comece por fora, não por dentro
Testes unitários em internals emaranhados são lentos de escrever e frágeis. Comece numa fronteira que você consegue observar:
- Um endpoint HTTP: envie uma requisição, registre a resposta.
- Um estado do banco de dados: execute uma operação, faça asserções sobre as linhas que mudaram.
- Uma página renderizada ou um fluxo de ponta a ponta: cadastrar, finalizar a compra, candidatar-se, pagar.
Esses testes são grosseiros, mas pegam mudanças que os usuários conseguem ver. Eu sempre cubro primeiro os fluxos de dinheiro: checkout, cobrança, cadastro, qualquer coisa que movimente valores ou deixe pessoas sem acesso.
Registre, depois faça a asserção
O processo é simples:
- Chame o código com entradas realistas.
- Anote a saída, mesmo que pareça errada.
- Transforme essa saída numa asserção.
- Repita para os casos de borda que você encontrar em logs, tickets de suporte e relatórios de bug antigos.
Se encontrar um bug real nesse processo, anote, mantenha o teste verificando o comportamento atual e corrija depois, num commit próprio. Misturar correções de bug numa refatoração torna impossível saber o que mudou.
Use a produção como fonte de dados de teste
Amostras sanitizadas de requisições de produção mostram os casos que o código realmente trata. Remova os dados pessoais antes que eles cheguem a uma fixture.
Quebre dependências só o necessário
Quando o código é impossível de testar porque chama uma API de pagamento ou o relógio do sistema, introduza uma costura fina: passe a dependência como parâmetro ou envolva-a numa função pequena que você possa substituir nos testes. Mantenha essas mudanças de costura minúsculas e mecânicas, já que são o único tipo de edição que você faz antes de ter testes.
Por onde começar a refatorar?
Comece onde o código muda com frequência e os erros são caros. Código feio em que ninguém mexe pode continuar feio.
Quando classifico os candidatos, estes são os sinais que observo, mais ou menos nesta ordem:
- Frequência de mudança. O histórico do controle de versão mostra quais arquivos são editados a cada sprint; estrutura bagunçada ali cobra um imposto repetidas vezes.
- Concentração de bugs. Tickets de suporte e notas de incidentes apontam para os módulos que vivem quebrando.
- Risco. Fluxos de dinheiro e autenticação, onde falhas custam dinheiro ou confiança.
- Bloqueios do roadmap. Se os próximos três itens do roadmap mexem no mesmo módulo, comece por ele.
Minha regra prática: se um módulo é ao mesmo tempo alterado com frequência e assustador de alterar, ele vem primeiro.
Por onde não começar
- Renomear e reformatar a base de código inteira. Isso gera diffs ruidosos e conflitos de merge e não resolve nada.
- O código mais antigo, só porque é o mais antigo.
- Código que você planeja apagar em breve. Não dê polimento no que está prestes a sumir.
A refatoração deve ser guiada por evidências sobre custo e risco, não por gosto.
Como refatorar em passos pequenos enquanto entrega funcionalidades?
Você faz cada passo de refatoração pequeno o suficiente para ser mergeado no branch principal por conta própria e intercala esses passos com o trabalho em funcionalidades, em vez de pausar o roadmap. Na minha experiência, times que param tudo por um “trimestre de refatoração” muitas vezes perdem a confiança dos stakeholders e terminam com um branch pela metade.
Use a regra do “deixe melhor” no ponto de trabalho
Quando uma funcionalidade exige mexer num módulo bagunçado, gaste um esforço limitado deixando essa área mais fácil de mudar primeiro e depois construa a funcionalidade. Kent Beck resume assim: “torne a mudança fácil, depois faça a mudança fácil.” A funcionalidade justifica a refatoração, e o negócio enxerga as duas.
Mantenha os passos reversíveis
Bons passos pequenos são assim:
- Extraia uma função, rode os testes, faça o merge.
- Mova uma função para um módulo melhor, rode os testes, faça o merge.
- Introduza uma nova implementação atrás de uma flag, mande um pouco de tráfego para ela, compare os resultados, depois faça a troca e apague a antiga.
Esse último padrão é o jeito mais seguro que conheço de mudar lógica arriscada: se o caminho novo se comportar mal, normalmente dá para voltar sem um deploy.
Separe os commits por intenção
Mantenha separados os commits de refatoração e os de mudança de comportamento. Assim, quem revisa consegue ler um commit de refatoração sabendo que nada deveria mudar, e um teste falhando avisa na hora que algo mudou.
Como isso foi no meu time
Na Inovaula, construí a v2 da plataforma web e depois liderei o time, e chegamos ao fluxo de entregas mais estável que o time já tinha tido. Olhando para trás, atribuo isso menos a alguma grande limpeza e mais ao hábito descrito acima: mudanças pequenas, revisadas com cuidado, mergeadas com frequência, com a estrutura melhorando como parte do trabalho normal.
Quando a refatoração deve dar lugar a uma migração?
A refatoração melhora o código dentro de uma stack, mas não resolve uma stack que é, ela própria, o obstáculo. Considere uma migração quando:
- O runtime, o framework ou bibliotecas importantes não recebem mais atualizações de segurança.
- Você não consegue contratar, ou o onboarding demora demais, por causa da tecnologia.
- A arquitetura impede qualquer teste, por exemplo lógica que vive no banco de dados ou em templates sem costuras.
- Cada refatoração exige tanta estrutura de apoio que, na prática, você já está reconstruindo.
Mesmo assim, prefiro a migração incremental à reescrita big-bang. A regra que sigo: colocar um chaveamento na frente de tudo antes de escrever código novo. Uma camada de roteamento, um proxy ou feature flags permitem mandar uma fatia do tráfego para o sistema novo enquanto o antigo continua atendendo todo o resto. Uma reescrita normalmente custa mais e demora mais, porque o sistema antigo precisa continuar rodando em paralelo o tempo todo.
Na avanzzada, liderei a migração completa de uma plataforma que se candidata automaticamente a vagas de emprego para seus usuários. Toda a arquitetura saiu de uma stack legada para uma nova enquanto as pessoas continuavam usando todos os dias. O que tornou isso viável foram os hábitos deste guia: saber o que o sistema atual faz, colocar o chaveamento primeiro e mover uma peça de cada vez.
Quer uma segunda opinião sobre a sua base de código?
Se você está diante de uma base de código antiga e sem saber se deve refatorar ou migrar, mande um email para me@filipeeduardo.dev com uma descrição curta ou um link, e eu digo o que verificaria primeiro.
Perguntas frequentes
Qual a diferença entre refatorar e reescrever?
Refatorar muda a estrutura em passos pequenos enquanto o comportamento continua o mesmo e o sistema segue rodando. Uma reescrita substitui o código por inteiro, muitas vezes em paralelo, e normalmente demora mais e traz mais risco, porque você precisa redescobrir cada comportamento que o código antigo tratava.
Dá para refatorar código legado sem testes?
Só nas menores mudanças mecânicas, como renomear com uma ferramenta em que você confia ou adicionar uma costura. Para todo o resto, escreva testes de caracterização primeiro, começando pelos caminhos de maior risco. Refatorar às cegas significa que você vai descobrir as quebras pelos seus usuários.
Quanto tempo um time deve dedicar à refatoração?
Não existe um número universal. Trate isso como parte do trabalho em funcionalidades, mais um trabalho direcionado nos módulos que causam incidentes repetidos, e evite projetos de refatoração separados sem um resultado de negócio visível.
Devo usar ferramentas de IA para refatorar código legado?
Elas ajudam em edições mecânicas e a explicar código desconhecido, mas não conhecem as suas regras de negócio escondidas. Trate o que elas produzem como o pull request de um júnior: rode contra os seus testes de caracterização e revise cada diff antes do merge.
Como saber se a refatoração não quebrou nada?
Os testes que passavam antes continuam passando depois, mais o monitoramento de produção. Para lógica arriscada, compare os caminhos antigo e novo atrás de uma flag antes de fazer a virada. Nenhum processo elimina o risco, mas este o mantém pequeno e visível.

