Pular para o conteúdo

Builder OS

Builder · OS
L09 · Teste-ouro (evals)
~11 MIN DE LEITURA

Lição 9 de 13: O teste-ouro

lição 9/13 do Módulo 3
AO FIM, VOCÊ VAI TER
  • Uma fixture-ouro versionada no repo: um input de exemplo + a saída esperada, em arquivo
  • Um assert que compara a saída do agente com a esperada e devolve passou/falhou
  • A fixture rodando como check do loop de verificação da L08 (não num canto solto)
  • O hábito: todo bug vira um caso-ouro novo antes de você consertar
  • Critério pessoal de quando um caso merece virar fixture (fidelidade de dado, dinheiro)

A L08 te deu o loop de verificação que confirma que esta mudança ficou certa. Esta lição é o que protege esse "certo" das mudanças que vêm depois.

O loop da L08 verifica agora; o eval verifica daqui a um mês

Na L08 você fechou o loop: o Claude roda o check e itera até passar, e você exige o output em vez de aceitar "feito". Esse loop confirma que a fatia que você acabou de mexer ficou certa.

Falta o outro lado. Você consertou o parser do CSV hoje, a suíte ficou verde, commit. Três semanas depois, numa mudança que parecia não ter nada a ver, o parser volta a dropar a última linha de arquivos sem quebra final. Ninguém viu. O build passou, porque nenhum teste cobria exatamente aquele arquivo. A app "parece pronta". Isso é : o que já funcionava volta a quebrar, e nada acende.

O remédio é um seu: você pega o caso que já funcionou, congela o input e a saída correta num arquivo, e re-roda esse arquivo a cada mudança. Se a saída mudar, o assert acende. O comportamento que já estava certo ganha um guarda permanente.

Eval e unit test cobrem coisas diferentes

Os dois rodam no mesmo comando e devolvem passou/falhou, mas julgam coisas diferentes.

Um unit test pergunta: esse pedaço de código faz o que eu disse? Dado 2, a função devolve 4. É lógica determinística, e quando passa, passa.

Um eval pergunta: essa geração ou esse parse continua fiel ao que eu espero, caso a caso, ao longo do tempo? Ele existe pros casos em que "parece pronto" mente, onde a saída tem a forma certa mas o conteúdo escorregou pro errado:

  • o parser leu o arquivo, devolveu uma lista de linhas, menos uma (drop silencioso de linha);
  • o cast de valor truncou 1234.56 pra 1234 porque alguém trocou o tipo;
  • a extração de um documento puxou o campo vencimento, mas pegou a data de emissão porque o layout mudou um pouco.

Em todos, o código rodou sem erro. Nenhuma exceção, nenhum stack trace. Um unit test que só checa "a função retornou uma lista" passa feliz. O eval pega porque ele compara a saída inteira com um gabarito que você já confirmou correto: ele olha o conteúdo, não só a forma.

A fixture-ouro, concreta

Uma são dois arquivos: um input de exemplo e a saída esperada que você conferiu na mão e declarou correta. O assert roda o seu código sobre o input e compara o resultado com a saída esperada.

Vai pelo seu produto. O exemplo abaixo é um parser de CSV, mas a forma é a mesma pra extração de campos de um documento (input: o documento; esperado: os campos certos) ou pra resposta de uma API oficial (input: o JSON cru; esperado: as linhas parseadas).

1. Guarde o input e o esperado como arquivos. Num diretório de fixtures do projeto:

entrada-01.csv
esperado-01.json

O entrada-01.csv é um trecho pequeno de dado real (3-5 linhas bastam, incluindo o caso chato: a linha sem quebra final, o valor com vírgula decimal, o campo vazio). O esperado-01.json é o que o parser deve produzir a partir dele; você confere uma vez, na mão, e congela.

2. Escreva o assert que compara os dois. Peça ao Claude, nomeando que a comparação é da saída inteira contra o gabarito:

prompt · text
Cria um teste que lê tests/fixtures/recibos/entrada-01.csv, roda o parser
de verdade nele, e compara o resultado com tests/fixtures/recibos/esperado-01.json.
Compara a saída INTEIRA (número de linhas e cada campo de cada linha), não só
o tipo. Se divergir, o teste falha mostrando QUAL linha e QUAL campo diferiu.
Roda e me mostra ele verde.

O prompt manda o Claude escrever e rodar o teste, então você não precisa configurar nada à mão. Por baixo, quem executa é o runner de testes da sua stack: pytest no Python, vitest/jest no Node, go test no Go. É o programa que encontra o arquivo de teste, carrega o input e o esperado, roda o seu parser de verdade, e checa resultado == esperado. Se o seu projeto ainda não tem runner instalado, peça ao Claude no mesmo prompt: "instala e configura o runner de teste da stack deste projeto antes de rodar." A partir daqui, qualquer mudança futura que faça o parser dropar uma linha ou truncar um valor vira vermelho, não silêncio.

Todo bug vira um caso-ouro antes do conserto

Aqui o eval encontra o TDD com agente da L08. Lá, você escreve um teste vermelho que reproduz o bug antes de consertar. O hábito do eval é o mesmo gesto, virado pra fidelidade de dado: toda vez que você acha uma regressão, ela vira uma fixture nova antes do conserto.

O fluxo:

  1. Você descobre que o parser dropou a linha de um CSV específico. Em vez de só consertar, salva aquele CSV como entrada-02.csv e a saída correta como esperado-02.json.
  2. Roda o eval: ele fica vermelho nesse caso novo (o bug está vivo, o gabarito prova).
  3. Deixa o Claude consertar até o caso 02 ficar verde sem quebrar o 01.
  4. Commit. Agora aquele bug nunca mais volta em silêncio: o caso 02 fica de guarda pra sempre.

Depois de alguns meses, sua pasta de fixtures guarda cada caso difícil que o seu domínio já produziu, e nenhum deles volta a quebrar sem o eval acender.

prompt · text
Achei um caso que o parser erra: este CSV (anexo como entrada-02.csv) tem a
última linha sem quebra final e o parser está dropando ela. ANTES de consertar:
salva esse CSV em tests/fixtures/recibos/entrada-02.csv, gera o esperado-02.json
com as 4 linhas CORRETAS (incluindo a última) — e me mostra ele pra eu conferir
antes de congelar — e adiciona o caso 02 ao eval.
Roda e me mostra o caso 02 FALHANDO. Só depois disso a gente conserta.

Quando o Claude gera o esperado, ele não é gabarito ainda: você confere na mão antes de congelar, como fez com o esperado-01.json. Se você congelar um esperado gerado sem olhar, o golden herda o mesmo bug que você quer pegar, e o eval vira verde mentindo. O caso 01 (você confere na mão) é o caminho canônico; a geração só economiza digitação depois que seus olhos passaram.

Onde o eval é obrigatório

Nem toda função precisa de uma fixture-ouro. Ela paga o custo onde "parece pronto" mente mais, nas zonas de perigo que você já viu:

  • Dinheiro / centavos (L02, L08): um cast que trunca 1234.56 pra 1234, ou um float que transforma R$ 10,30 em 10.299..., não levanta exceção. Uma fixture com valores que têm centavos, comparada inteira, pega o drift no momento em que ele entra. A L08 te deu asserts que checam uma regra que vale sempre (por exemplo, "centavos nunca é negativo"). A fixture-ouro vai além: ela guarda o valor exato que aquele input tem que produzir: não só "passa na regra", mas "dá R$ 12,34, esse número e nenhum outro".
  • Fidelidade de dado (scraper, parse, extração): a soma das partes que tem que bater com o total; o número de linhas que tem que ser o mesmo da fonte; o campo que não pode trocar de coluna quando o layout muda um pouco. Para produto de dados públicos (um scraper da Câmara, um parser de Diário Oficial, uma extração de campos de um documento oficial), o eval é o que separa "rodou" de "rodou certo". O dado errado num desses não dá erro: ele entra no banco, vai pro gráfico, e vira um número que alguém acredita.

O critério prático: se a saída tem a forma certa mas o conteúdo pode escorregar pro errado sem ninguém ver, é caso de fixture-ouro.

Plugue o eval no loop da L08

Uma fixture-ouro num arquivo que você esquece de rodar não protege nada. O lugar dela é dentro do check que a L08 já instalou: o comando único de pass/fail (algo como npm run check ou pytest) que o Stop hook da L08 dispara automaticamente quando o Claude diz que terminou. (Lembrete da L08: o Stop hook é o gatilho que roda esse comando no fim de cada turno, sem você pedir.)

Se o seu check já roda os testes do projeto, o eval é só mais um teste que esse mesmo comando roda. Você não precisa montar um disparo novo: basta colocar a fixture na pasta de testes que o check já varre, e ela passa a rodar junto. Se o seu projeto ainda não tem esse check da L08 montado, monte ele primeiro: esta lição assume que o loop da L08 já está no lugar.

Você terminou quando

Você tem, no seu próprio projeto, uma fixture-ouro viva e plugada no loop. Concretamente, você:

  • guardou um input de exemplo e a saída esperada como arquivos versionados (não valores soltos dentro do teste);
  • escreveu um assert que compara a saída inteira com o esperado e acende vermelho quando diverge, dizendo qual linha e qual campo;
  • confirmou que esse eval roda dentro do check da L08 (mesmo comando, mesmo Stop hook), não num canto solto;
  • adotou o hábito: o próximo bug de fidelidade que você achar vira um caso-ouro novo antes do conserto.

Auto-checagem: você consegue responder?

  1. Qual a diferença entre o loop de verificação da L08 e o eval desta lição, e o que cada um protege?
  2. Por que um unit test que checa "o parser retornou uma lista" não pega um drop silencioso de linha, e o eval pega?
  3. Por que comparar a saída inteira contra o gabarito importa, e não uma amostra?
  4. Qual o gesto quando você encontra uma regressão, e por que ele transforma o bug num guarda permanente?