portfólio

Migração de suíte legada: de Shell para Java

Java Gradle Shell Script Python SQL

Papel: estratégia de migração, desenho das PoCs e condução da execução
Contexto: automação de testes de billing na Globo, via NTConsult
Período: mar – ago/2026

O problema

Uma suíte de regressão de billing com mais de uma década de lógica acumulada, escrita inteiramente em Shell Script: um orquestrador monolítico, bibliotecas de funções, arquivos de configuração por ambiente, SQL embutido no meio do código, menus interativos e os cenários de negócio. Funcionava. E era o único lugar onde parte do conhecimento de negócio da empresa estava registrada — em código que ninguém conseguia ler com segurança.

Migrar isso para Java tem um modo de falha conhecido: reescrever com base no que você acha que entendeu, descobrir tarde que o legado fazia algo a mais e entregar uma suíte que passa enquanto o sistema quebra. O trabalho de verdade não é escrever Java. É saber exatamente o que o legado faz — inclusive o que ele faz sem querer.

Como conduzi

1. Descoberta antes de arquitetura

Antes de propor qualquer arquitetura, fiz engenharia reversa sistemática do monolito e registrei o resultado como artefato versionado:

  • um mapa do repositório — árvore, dependências entre scripts e pontos de entrada reais
  • um inventário de funções de toda a base, das bibliotecas menores até o orquestrador principal, consolidado numa tabela única
  • um mapa dos menus interativos, que escondiam boa parte dos caminhos de execução
  • um inventário de dados — variáveis, variáveis de ambiente, arquivos de configuração e todo SQL embutido
  • contratos de entrada e saída — binários invocados, APIs chamadas, formatos de payload e de arquivo
  • notas de comportamento — pré-condições, validações e caminhos de erro

O artefato mais importante foi um relatório de descoberta declarando totais, ambiguidades e lacunas. Não “entendemos o sistema”, mas “entendemos estas partes, estas outras estão ambíguas, e sobre estas não sabemos nada”. Lacuna nomeada vira tarefa; lacuna silenciosa vira incidente.

A partir dessa descoberta, produzi a documentação que serviria de critério de aceitação da migração: os cenários estruturados e classificados, um dicionário de dados, regras de negócio agrupadas por área, regras de validação de payload e o mapeamento da configuração multiambiente. No caminho, corrigi contagens de situação erradas que tinham sobrado da documentação anterior — detalhe pequeno de consequência grande, já que número errado em documentação de referência se propaga em silêncio para todo mundo que confia nela.

Antes de migrar qualquer coisa, tornei a suíte legada segura de tocar: estabeleci uma linha de base com análise estática de shell e testes automatizados dos scripts, e só então apliquei melhorias — remoção de invocações redundantes, troca de condicionais aninhadas por estruturas de seleção, extração de funções auxiliares e substituição de números mágicos por constantes nomeadas. Refatorar sem teste é chute; linha de base primeiro, refatoração depois, é a diferença entre engenharia e sorte.

2. PoC 1 — o legado pode virar um cliente tipado?

Uma aplicação Java construída com Gradle (Kotlin DSL): um cliente HTTP para a API de billing, uma interface de linha de comando e DTOs tipados para conta, assinatura e forma de pagamento, substituindo a montagem de payload por concatenação de string do shell. 32 classes, com testes.

Durante essa PoC encontrei um token de autenticação fixo no código e corrigi, movendo-o para injeção por variável de ambiente ou parâmetro de linha de comando. Achado de segurança em PoC vale exatamente o mesmo que achado em produção.

3. PoC 2 — a lógica de negócio consegue sair do shell e virar domínio testável?

Essa era a pergunta mais difícil. Portei uma rotina crítica de validação de conciliação financeira — liquidação, chargeback, estorno e reversão, incluindo parcelamento — para um domínio Java puro:

  • separação estrita entre domain (imutável, sem I/O), service (lógica pura) e io (adaptadores de sistema de arquivos)
  • um objeto de valor para dinheiro, evitando aritmética de ponto flutuante sobre valor monetário — virou o tipo mais usado do projeto
  • um tipo selado para o resultado da validação, com sucesso e divergência como variantes explícitas, tornando impossível esquecer de tratar a falha
  • um builder com validação, para que o contexto de execução nunca possa ser construído em estado inválido
  • um conversor dos arquivos de configuração legados para um formato estruturado

Resultado — e esta é a frase de maior valor do case inteiro: 38 testes passando sem banco, sem a plataforma de billing instalada e sem manipulação de relógio. O que torna isso possível importa mais do que a frase: a data, que antes vinha da leitura do relógio do sistema operacional, passou a ser parâmetro de entrada explícito das camadas de domínio e de serviço, coerente com manter o domínio livre de I/O. O teste entrega a data de referência que o cenário precisar, e o resultado é determinístico — nada precisa mockar nem congelar o relógio da máquina. Lógica que antes exigia o ambiente inteiro de pé passou a rodar em memória.

Conduzi essa PoC em fases, incorporando rodadas de revisão de código ao longo do caminho.

4. Migração para a suíte compartilhada

Com as duas PoCs validando a abordagem, migrei os testes de regressão do domínio fiscal para a suíte Java compartilhada da organização: camada de acesso a dados, DTOs, endpoints de consulta e escrita, e os próprios cenários de regressão.

Duas decisões que considero as mais maduras do projeto inteiro:

Desenhei uma comparação explícita entre execução legada e execução migrada, submetida a revisão adversarial de especificação antes de virar plano. Migração sem prova de equivalência é fé.

Registrei uma divergência de comportamento como decisão consciente, em vez de forçar a versão migrada a imitar o legado. Um cenário de retentativa se comportava de forma diferente; documentei a divergência, o raciocínio e a escolha, e segui. Divergência documentada é decisão de engenharia; divergência escondida é dívida.

5. Como o código de teste foi gerado — e o que eu recusei do agente

A escrita dos cenários migrados foi conduzida com agentes de IA, e o encadeamento importa mais do que a ferramenta: spec antes de plano, plano aprovado antes de código, implementação contra um plano já revisado. O agente não começa a escrever teste porque alguém pediu “escreva os testes” — ele escreve contra um plano que passou por revisão humana. E o desenho dos cenários veio antes disso, do planejamento e do teste manual: o agente entrou na etapa de transformar cenário já pensado em código, não na de decidir o que testar.

O ferramental foi GitHub Copilot no VS Code, com o sistema, a automação dele e o repositório de testes abertos no mesmo workspace — é o que permite relacionar o teste com o código que está sendo testado —, o Superpowers conduzindo o encadeamento e o Serena (MCP) acrescentando navegação estrutural por símbolos via LSP, para que “quem chama este método” seja respondido por referência real e não por trecho parecido.

Medi o consumo request a request em vez de declarar ganho de produtividade — sem baseline manual para comparar, qualquer percentual de ganho seria chute com cara de dado.

E teve o que não entrou. Entre as propostas do planejamento gerado por modelo, uma era desativar um cenário que estava falhando, em vez de fazê-lo passar. Teste desativado deixa o pipeline verde, derruba a cobertura em silêncio e só aparece quando o bug chega em produção. Agente propondo mexer no status de um teste em vez de mexer no código é sinal de parada — sempre revisão humana.

A prova de equivalência entre execução legada e migrada, descrita acima, eu só fui descobrir depois que tinha nome: Parity-Driven Development. Cheguei nela por necessidade, não por leitura.

Escrevi o método completo, a tabela de consumo e as limitações do dado em 195 requests para gerar testes com IA. E nenhum ganho de produtividade para declarar.

O que isso demonstra

  • Modernização de legado ponta a ponta: descoberta → documentação → PoC → migração → validação de equivalência
  • Engenharia reversa metódica, com lacunas declaradas em vez de assumidas
  • Modelagem de domínio em Java moderno: imutabilidade, objetos de valor, tipos selados, construção validada
  • Capacidade de desacoplar lógica de negócio da infraestrutura — transformando teste lento e frágil em teste determinístico
  • Postura de segurança: credencial fixa tratada como defeito, não como detalhe de PoC
  • Uso disciplinado de agente de IA na geração de teste: encadeamento spec → plano → implementação, consumo medido request a request e proposta do agente recusada quando cabia

Stack

  • Java — camada de domínio e de serviço nas duas PoCs e na migração para a suíte compartilhada
  • Gradle (Kotlin DSL) — build da PoC do cliente tipado
  • Shell Script — a suíte legada em migração, e alvo da rede de segurança de análise estática e testes
  • Python — conversão dos arquivos de configuração legados
  • SQL — embutido por toda a suíte legada, inventariado durante a descoberta