O que nove tickets de TDD me ensinaram sobre revisar código antes de escrever código

O que nove tickets de TDD me ensinaram sobre revisar código antes de escrever código

Passei as últimas semanas construindo um pipeline de análise financeira em Python — coleta de dados de mercado, análise de sentimento com FinBERT, geração de sinais de trading e relatórios em múltiplos formatos. O projeto em si é interessante, mas não é sobre ele que quero falar.

O que ficou foi o processo. Cada componente passou por um ciclo de revisão TDD antes de qualquer linha de implementação ser escrita. Depois de nove tickets assim, alguns padrões ficaram tão óbvios que eu precisava registrar.

Cada rodada de revisão enxerga uma camada diferente

No início, achei que revisar uma spec uma vez era suficiente. Estava errado.

O que aprendi foi que cada rodada de revisão tem uma "lente" natural:

  • Rodada 1 pega gaps estruturais — arquivo faltando, tipo não definido, dependência errada
  • Rodada 2 pega problemas de design — casos de borda ignorados, integração com o restante do pipeline
  • Rodadas 3+ pegam contradições entre spec e realidade — constante com valor errado, campo que não existe no dataclass upstream, pseudocódigo que chama função inexistente

No ticket de tokenização (T5), a spec parecia razoável na primeira leitura: oito critérios de aceite, três backends documentados (tiktoken, HuggingFace tokenizers, sentencepiece). Mas quando carreguei os tipos reais do ticket anterior — o FusedRecord que o módulo de fusão produz — descobri que "fused analysis text" era completamente indefinido. Não havia contrato entre a saída estruturada de T4 e a entrada texto-puro de T5.

Isso só apareceu na segunda rodada, quando eu parei de ler a spec isolada e fui ler o @dataclass do módulo upstream.

A regra que passei a seguir: nunca revisar um ticket sem abrir as definições de tipo dos módulos dos quais ele depende.

Dependency analysis como fase formal

O padrão mais eficaz que emergiu nos últimos tickets foi fazer uma análise de dependências explícita antes de qualquer implementação. Não como checklist mental — como passo documentado.

No ticket de sinais de trading (T7), esse passo encontrou três problemas que a revisão de spec tinha ignorado:

# Pseudocódigo na spec
combined_score = clamp(0.5 * sentiment + 0.5 * market_signal, -1.0, 1.0)

# Realidade: clamp() não existe no codebase
# Correto:
combined_score = max(-1.0, min(1.0, 0.5 * sentiment + 0.5 * market_signal))

Além disso, a variável de resultado do Stage 5 estava fora de escopo no Stage 6 do pipeline.py. E MarketData.volume era declarado como int no dataclass, mas json.load() devolve float para todos os números — então isinstance(data.volume, int) quebrava silenciosamente na validação.

Esses três problemas não apareceram na revisão da spec porque a spec não mencionava clamp() como algo a verificar, não mostrava o escopo das variáveis no pipeline, e não discutia serialização JSON. Só apareceram quando eu abri os arquivos referenciados e li o código real.

O terceiro critério de aceite é sempre vago

Isso é quase uma lei empírica a essa altura. Em todos os nove tickets, o terceiro AC estava mal especificado. Sem exceção.

Compara:

Vago: "resultado inclui rationale legível por humanos"

Concreto: "string no formato Sentiment positive (0.16) with confidence 0.80. Market return: -0.23%. Combined score: 0.07. Signal: hold."

A versão concreta é verificável por cálculo manual. A vaga depende de interpretação subjetiva de "legível". Quando cheguei no ticket de geração de relatórios (T8), o AC de consistência cross-formato levou cinco rodadas de revisão — mais que qualquer AC de formato único. O problema era que "mesma contagem de dados em texto, JSON e HTML" escondia três definições diferentes de "linha de dado": o texto tinha cabeçalho e separadores, o HTML excluía a linha de header do <tr>, o JSON era simplesmente o tamanho do array.

O que aprendi: quando revisar o terceiro AC, assuma que está subespecificado e exija valores concretos ou schema de saída exato.

Revisão de spec e revisão de código pegam bugs diferentes

Mesmo com spec revisada em múltiplas rodadas, a revisão de código encontrou problemas reais em todos os tickets. Os dois processos não são substituíveis.

Alguns que me surpreenderam:

No ticket do cliente HTTP (T1), métricas de latência acumulavam indefinidamente — nunca havia reset. O contador crescia sem limite durante a vida do processo.

No módulo de sentimento (T6), descobri que MagicMock.__call__ = func é silenciosamente ignorado em instâncias. O __call__ é resolvido no tipo, não na instância. A correção é usar side_effect:

# Errado — silenciosamente ignorado
mock_model.__call__ = lambda x: fake_output

# Correto
mock_model.side_effect = lambda x: fake_output

No gerador de relatórios (T8), havia um bug crítico de runtime: getattr(result, "txt") — mas o campo se chama text, não txt. Esse tipo de coisa passa em qualquer revisão de spec porque a spec não mostra o nome exato do atributo. Só aparece quando você lê o código.

O que mudaria se começasse de novo

Três coisas concretas:

Primeiro, adicionaria "ler o pipeline.py" como passo obrigatório em toda revisão. Gaps de integração — variável fora de escopo, handler de exceção que engole o erro silenciosamente — só aparecem quando você lê o ponto de entrada real.

Segundo, criaria um shared constant para o padrão de nome de arquivo dos registros fundidos ({ticker}_{date}.json). Hoje esse padrão está duplicado em FusedRecordWriter e em find_historical_fallback(), mantidos em sincronia manualmente. É uma bomba-relógio.

Terceiro, adicionaria __repr__ ao MockTensor. Quando um teste falha hoje, o output mostra <test_sentiment.MockTensor object at 0x7f...>. Inútil. Com __repr__ mostrando os valores reais, o tempo de debug cai pela metade.


O investimento em revisão de spec antes de implementar parece overhead até você perceber que está pagando o custo de qualquer jeito — só que depois, na forma de bug em produção ou refactor doloroso. Nos primeiros tickets eu implementava rápido e revisava pouco. Nos últimos, revisava mais e implementava mais limpo. A conta fecha.

← IA agêntica já está no seu codebase — e a maioria dos times não está preparada