O legado não mente

O código legado não tem documentação. E quando tem, é de três anos atrás. Os testes não existem, ou, se existem, estão quebrados e marcados com skip. Os comentários contradizem o código. O autor original já saiu da empresa, e quem sobrou só deixou o aviso: “se mexer, explode”.

E, no entanto, esse código está rodando neste exato momento. Processa pagamentos, recebe logins, registra pedidos.

A documentação mente. Os comentários mentem. A memória das pessoas mente ainda mais. A única coisa que não mente é o tráfego que realmente está fluindo.

Então, onde devemos procurar a especificação? Não no wiki. Não no Confluence. No nginx access log.

O ovo e a galinha

Para refatorar o legado, você precisa de uma rede de segurança. Quando você muda alguma coisa, precisa saber na hora se o comportamento mudou. Essa rede de segurança é, justamente, o teste.

Mas o legado não tem testes. Para escrever testes, você precisa saber o que o código faz. Para saber o que o código faz, precisa lê-lo. E ao lê-lo, descobre que não há nem testes nem documentação.

O que vem primeiro, o ovo ou a galinha? É o impasse clássico que Michael Feathers batizou em Working Effectively with Legacy Code. Como resposta, ele propôs o characterization test (teste de caracterização) — um teste que não fixa o que o código deveria fazer corretamente, mas o que ele de fato faz agora. Certo e errado ficam para depois. Primeiro é preciso fixar o comportamento atual para poder pôr a mão.

Na época de Feathers, isso era escrito à mão. Você chamava a função, observava o valor que saía e o transcrevia literalmente para o expected. Tedioso, lento e, por isso, ninguém levava até o fim.

Mas, no nível da API, esse “resultado de ter chamado a função” já está acumulado em algum lugar. Todos os dias, aos milhares. Dentro dos arquivos de log.

Um mês de logs é a especificação

Coletando durante um mês, é possível capturar quase todo o comportamento atual de uma API legada.

nginx access log (1 mês):
  endpoint · HTTP method · status code · timing
  frequência de chamadas → prioridade
  padrões de erro (401, 422, 500 …)

request/response body (capturado via middleware ou reverse proxy):
  pares requisição/resposta normais → comportamento que deve passar
  pares requisição/resposta de erro → edge cases que não podem quebrar

Unindo essas duas vertentes, traduz-se diretamente em testes de integração Hurl. Hurl é um formato que escreve a requisição HTTP e a resposta esperada literalmente, em texto puro. Um par de tráfego — “para esta requisição saiu esta resposta” — é exatamente um bloco Hurl.

# POST /api/orders — frequência de chamadas #3, 12 mil por dia
POST https://api.example.com/orders
Content-Type: application/json
{ "sku": "A-1024", "qty": 2 }

HTTP 201
[Asserts]
jsonpath "$.order_id" exists
jsonpath "$.status" == "pending"
jsonpath "$.total" == 49800

Esse teste não sabe “como a API de pedidos deveria funcionar”. Ele apenas sabe que “agora ela funciona assim”. E isso basta. No instante em que a refatoração mudar essa resposta, a luz vermelha acende.

O que se deriva automaticamente dos logs:

  • Quais endpoints são realmente usados → um endpoint chamado 0 vez em um mês é código morto. Candidato a exclusão antes da refatoração.
  • Padrões de resposta normais → testes de regressão básicos.
  • Padrões de erro → os edge cases verdadeiros que ninguém consegue imaginar. São os 422 e 500 que usuários reais produziram.
  • Frequência de chamadas → prioridade dos testes. Começa-se pelo que tem 12 mil chamadas por dia.

O último item é importante. Quando uma pessoa escreve testes, começa pelo happy path que ela lembra. O tráfego não tem esse viés. O caminho que de fato recebe carga é, por si só, a prioridade.

Uma rede de segurança em duas camadas

Esta abordagem não é usada de forma isolada; é uma camada do pipeline ratchet que eleva o legado a agent-operable.

nginx log (1 mês) → geração automática de Hurl → fixa o comportamento atual da API legada
        ↓
tsma → rede de segurança em nível de função (unit)
        ↓
filefunc → organização da estrutura do código (um conceito por arquivo)
        ↓
refatoração → Hurl verifica a preservação do comportamento da API (integration)

O essencial é que a rede de segurança tem duas camadas.

  • tsma = rede de segurança em nível de função. Detecta se a lógica interna mudou. Mas, mesmo com a assinatura da função intacta, o comportamento de todo o endpoint pode mudar.
  • Hurl from traffic = rede de segurança em nível de API. Detecta se o contrato visto de fora é preservado. Não importa como você reescreva o interior: se o que entra de fora e o que sai para fora forem iguais, passa.

Refatoração é, por definição, “mudar a estrutura interna preservando o comportamento externo”. Sendo assim, a definição do “comportamento externo” a ser preservado precisa estar fixada em algum lugar. tsma segura a fronteira interna, e Hurl segura a fronteira externa. Só quando as duas camadas estão juntas é que se pode dizer ao agente: “reescreva à vontade; o que quebra, a máquina vê”.

Um árbitro que não sabe bajular

Isso se encaixa exatamente com a essência do Symbolic Feedback Loop.

Se você perguntar ao agente “refatorou bem?”, ele responde “sim, organizei tudo de forma limpa”. Quando se pede uma opinião, ele bajula. Mas, ao rodar o Hurl, sai POST /orders → expected 201, got 500. Números e status codes não bajulam. Porque não têm emoção.

O teste Hurl extraído do tráfego é uma especificação sem a intervenção do julgamento humano. Não é “alguém acha que deveria funcionar assim”, e sim “a observação mostrou que funcionou assim”. Não é uma afirmação, é uma medição. Por isso, quem julga o certo e o errado da refatoração pode ser a máquina, e não a pessoa. O LLM não é o juiz, e sim o executor, e o julgamento cabe a uma ferramenta determinística.

A única premissa: logs bem registrados

Para que este método funcione, basta uma coisa. Um mês de logs bem registrados.

E “bem registrados” é tudo. O access log sozinho não basta. Ele dá o endpoint, o status code e o timing, mas não dá o cerne a ser fixado — o par de request body e response body. Saber apenas POST /orders → 201 não permite reproduzir “para esta entrada saiu esta saída”. Para fixar a funcionalidade, é preciso ter em mãos tanto o que entrou quanto o que saiu.

Então a verdadeira pergunta não é “como escrever os testes”, e sim “meus logs estão escritos bem o bastante para serem uma especificação”.

  • O request/response body é registrado, ou só fica o status code?
  • As respostas de erro também são registradas? Os bodies de 422 e 500 são justamente os edge cases que ninguém consegue imaginar.
  • O log é estruturado o bastante para que a máquina consiga emparelhar requisição e resposta?

Se isso estiver pronto, você já vinha escrevendo a especificação havia um mês. Não há necessidade de escrever testes à parte. O pipeline de logs já os escrevia por você. Se não estiver pronto, basta encaixar uma camada de middleware agora e deixá-la ligada por um mês. Um mês depois, todo o comportamento atual do legado estará na sua mão.

Por que um mês, e não um dia? Um dia só pega o happy path. Um mês pega o batch de fim de mês, o pico de tráfego às vésperas do fechamento, os endpoints administrativos chamados raramente, o cron que roda uma única vez às 3 da manhã — a cauda longa do sistema. A especificação não é a média, é a distribuição.

Traduzir os logs em Hurl para fixar a funcionalidade

Com os logs prontos, o resto é mecânico. Você joga um mês de pares request/response na ferramenta e traduz cada par em um bloco Hurl. As centenas de arquivos Hurl assim despejados são a suíte de characterization — uma rede de segurança que fixa por inteiro o comportamento atual do legado. Você não leu uma linha de código. Leu apenas o tráfego que passou.

Vale antecipar aqui um ponto em que muita gente costuma hesitar. “Mas o log tem dados pessoais, pagamentos e tokens — pode-se fixar isso num teste?”

Pode. Mais precisamente, não é preciso fixar. Porque esta metodologia, por natureza, não precisa dos valores. O que o characterization test fixa não são os valores, é o comportamento.

HTTP 201
[Asserts]
jsonpath "$.order_id" exists
jsonpath "$.total" == 49800

Aqui, o que importa como especificação não é o número 49800, e sim a estrutura “o campo total existe como inteiro e, para uma dada entrada, é calculado assim”. Mesmo mascarando os valores ou trocando-os por dados sintéticos, o valor da especificação quase não diminui. capture → mascaramento → geração de Hurl: todo esse pipeline roda dentro da sua infraestrutura. O log raw não tem por onde sair. O que sobra é uma especificação com os valores ocultos, um contrato em que só a estrutura foi preservada. O fato de não precisar enviar o log para fora não é uma concessão de segurança, é a essência desta abordagem — porque, de início, basta fixar o comportamento.

Rodando o Hurl gerado uma vez no staging, ali mesmo se separa o que passa do que falha. Se todas as luzes verdes acenderem, agora você pode começar a refatorar. Diga ao agente para reescrever à vontade; o que quebra, o Hurl vê.

Uma escada erguida sem código

Por isso, o verdadeiro valor desta abordagem não é “escrever testes rápido”. O verdadeiro valor é este.

  • Começa-se sem ler o código — o autor original foi embora e não há documentação, mas a rede de segurança se estende apenas com o tráfego que passou. Você ganha o direito de pôr a mão antes de entender o código.
  • O resultado é imediatamente verificável — rodando o Hurl gerado no staging, ali mesmo sai pass/fail. Não é “deve estar funcionando”, e sim “agora passam 327 de 327”.
  • Os dados não passam por cima do muro — do capture até a geração do Hurl, tudo termina dentro da minha infraestrutura. Quanto mais regulada a indústria, mais decisivo é poder começar sem mandar nada para fora.

A primeira pá da modernização do legado costuma travar no penhasco do “ninguém sabe qual é o comportamento atual”. Tráfego → Hurl ergue uma escada nesse penhasco. E, para erguer a escada, não é preciso código. Basta o tráfego que passou — e nem esse tráfego sai de dentro do muro.

O fluxo já vinha escrevendo a especificação

Nós nos esforçamos para escrever a especificação à parte. Escrevemos OpenAPI à mão, descrevemos o comportamento no wiki e, quando essas coisas divergem do código, chamamos de drift e lamentamos.

Mas o sistema vivo, a cada instante, vinha escrevendo sua própria especificação sozinho. Cada vez que uma requisição entra e uma resposta sai, isso é uma linha de autodescrição: “eu sou um sistema assim”. O arquivo de log é essa autobiografia acumulada por um mês.

Nós é que não a líamos.

O legado não é desprovido de documentação. A documentação está dentro do access log, só que num formato incômodo de ler para humanos. Traduzida em Hurl, ela se torna uma especificação executável, um contrato julgado pela máquina.

A documentação mente. O tráfego não mente.


Fontes / Fundamentação

Conceitos e ferramentas centrais

  • Michael Feathers. Working Effectively with Legacy Code. Prentice Hall, 2004. — Origem do conceito de characterization test. “Fixa-se não o que o código deveria fazer corretamente, mas o que ele faz agora.”
  • Projeto Hurl (hurl.dev) — Formato de teste de requisição/resposta HTTP em texto puro. Integrado como uma das 10 SSOTs do yongol.
  • Comprovação com 527 funções no tsma — ratchet em nível de função (tsma).

Extrair testes a partir do tráfego e da execução (carving / record-replay)

  • Elbaum, Chin, Dwyer, Jorde (2009). “Carving and Replaying Differential Unit Test Cases from System Test Cases.” IEEE TSE 35(1). — Base acadêmica dos differential unit tests que fazem record da execução do sistema e replay em nível de unidade.
  • Meta Engineering Team (2024). “Observation-based Unit Test Generation at Meta.” FSE 2024, arXiv:2402.06111. — Carving de testes a partir de observações da execução do app. 9,6 milhões de execuções no CI, 5.702 defeitos detectados. Comprovação em escala industrial de “a observação é o teste”.

Fixar o comportamento atual (snapshot / golden master)

  • Fujita, Kashiwa, Lin, Iida (2023). “An Empirical Study on the Use of Snapshot Testing.” ICSME 2023. — Comprovação da adoção de testes snapshot (= golden master/characterization). “Detecta-se a mudança fixando não a correção, mas a saída atual.”

Rede de segurança da refatoração

  • Kim, Zimmermann, Nagappan (2014). “An Empirical Study of Refactoring Challenges and Benefits at Microsoft.” IEEE TSE 40(7). — Comprovação de que, sem testes que garantam a preservação do comportamento, a refatoração é custo e risco.
  • Yoo, Harman (2012). “Regression Testing Minimization, Selection and Prioritization: A Survey.” STVR 22(2). — Teste de regressão = a definição padrão de “a confiança de que a mudança não fere o comportamento existente”.

A distribuição de uso real é a prioridade

  • John D. Musa (1993). “Operational Profiles in Software-Reliability Engineering.” IEEE Software 10(2). — Ao distribuir os testes por frequência de uso, mesmo interrompendo por questões de cronograma, a funcionalidade mais usada é a mais verificada. Base clássica de “distribuição de tráfego em vez do viés do happy path”.

Por que a máquina deve julgar (o LLM não é o juiz, e sim o executor)

  • Huang, Chen, Mishra, et al. (2024). “Large Language Models Cannot Self-Correct Reasoning Yet.” ICLR 2024, arXiv:2310.01798. — Sem feedback externo, o LLM não corrige o próprio raciocínio. A razão de ser necessário um verificador externo determinístico.

  • Sharma, Tong, et al. (2024). “Towards Understanding Sycophancy in Language Models.” ICLR 2024, arXiv:2310.13548. — O RLHF ensina a concordância e derruba a confiabilidade do autojulgamento do LLM.

  • Imagem de capa: gerada por IA (Google Gemini)

Leituras complementares

  • Michael Feathers, “Characterization Testing” — Texto do criador do termo. “No instante em que o software entra em produção, ele se torna sua própria especificação (it becomes its own specification).” Quase a mesma tese do título deste artigo.
  • Tutorial oficial do Hurl, “Your First Hurl File” — De GET / HTTP 200 até o modo --test. Uma introdução que coloca na sua mão a ideia de que uma linha de texto puro já é um teste.
  • GitHub Engineering, “Scientist: Measure Twice, Cut Once” — Biblioteca que executa simultaneamente o código legado (control) e o novo (candidate) em produção para comparar os resultados. “Só o comportamento real é a verdadeira especificação.”
  • Twitter Diffy (resumo da InfoQ) — Proxy que envia a mesma requisição aos serviços novo/antigo e pega como regressão apenas a diferença nas respostas. Precedente clássico de “fixar o comportamento sem escrever testes”.
  • GoReplay — Ferramenta que captura o tráfego HTTP vivo da interface de rede e o reproduz no staging. Implementação representativa de “tráfego de produção como entrada de teste”.
  • Nicolas Carlo, “Characterization vs Approval Tests” — Organiza os três termos para uma técnica essencialmente igual e enfatiza o papel do “Printer”, que faz o scrub de dados sensíveis na saída.
  • Pact — consumer-driven contract testing. Abordagem do “contrato explícito” em contraste com a fixação de tráfego. Olhar as duas vias juntas traz equilíbrio.

Artigos relacionados