
Como refatorar código sem testes?
Você herdou cem mil linhas de código legado. Sem testes. Quer refatorar, mas não sabe o que vai quebrar se mexer. Para escrever testes, precisa entender o código; para entender o código, precisa de documentação — que também não existe.
Ninguém mexe. Apodrece ainda mais.
Todo código legado do mundo está preso nesse impasse. Empresas da Fortune 500 gastam entre 60% e 80% do orçamento de TI na manutenção de sistemas legados. 42% do tempo dos desenvolvedores é consumido lidando com dívida técnica.
E se um LLM pudesse escrever os testes por você?
Os problemas de delegar testes ao LLM
Quando você pede ao LLM “escreva um teste para esta função”, algo sai. O problema é triplo:
Primeiro, ele não sabe por onde começar. Com 527 funções — começa pela primeira em ordem? Pela mais importante? Não há critério.
Segundo, não é possível verificar a qualidade do teste. O LLM escreveu um teste e ele passou. Mas esse teste realmente verifica o comportamento da função, ou é apenas uma chamada sem assert — uma casca vazia? Só lendo manualmente para saber.
Terceiro, sem feedback os testes do LLM param em 60–70%. Apenas “teste esta função” não alcança 100% de cobertura de branches. É preciso informar quais branches estão faltando para que ele complete.
O LLM não é incapaz de escrever testes. O problema é a ausência de uma estrutura que diga o que escrever e quão bem foi escrito.
tsma: um trilho de testes que funciona com um comando
tsma é uma ferramenta CLI que indexa todas as funções do projeto, detecta a presença de testes, mede cobertura e fornece feedback preciso para agentes LLM.
O único comando que o agente precisa conhecer:
$ tsma next
Esse único comando aciona o loop inteiro:
$ tsma next # mostra a próxima função sem teste
→ você escreve o teste
$ tsma next # detecta o novo teste, executa, mede cobertura
→ 100%? PASS, avança para a próxima função
→ <100%? mostra branches não cobertas com números de linha
$ tsma next # remede o teste corrigido
→ melhorou ou não, marca como DONE e avança
Repete até aparecer “All functions complete!”.
Validado em 527 funções
tsma foi aplicado a um projeto Go real (527 funções).
| Resultado | Qtd | Proporção |
|---|---|---|
| PASS (100% cobertura de branches) | 246 | 46,7% |
| DONE (melhor esforço) | 281 | 53,3% |
| TODO (não processado) | 0 | 0% |
246 funções alcançaram 100% de cobertura de branches. As 281 restantes não chegaram a 100%, mas tiveram testes escritos até o limite possível.
Por que algumas funções não alcançam 100%?
Funções que alcançam 100% e funções que não alcançam
Se uma função pode alcançar 100% de cobertura de branches depende de como ela recebe suas dependências.
Interface (mockable) — 100% alcançável:
type Handler struct {
svc AuthSvc // interface — substituível por mock
}
Injetando um mock no teste, você controla todos os caminhos:
svc := mocks.NewMockAuthSvc(ctrl)
svc.EXPECT().Login(...).Return(result, nil) // caminho de sucesso
svc.EXPECT().Login(...).Return(nil, err) // caminho de erro
Tipo concreto (not mockable) — 100% impossível:
type Handler struct {
svc *service.SMSImportService // ponteiro de struct — não substituível
}
A implementação real roda com dependências internas como banco de dados e APIs externas. Não é possível provocar um erro específico ou forçar um resultado específico. Branches que dependem desses resultados são inalcançáveis por testes unitários.
Resposta do tsma: após feedback sobre branches não cobertas, tenta mais uma vez. Se ainda não alcançar, aceita como DONE. Isso não é limitação da ferramenta, mas reflexo da testabilidade do código. Introduzir interfaces (DI) torna 100% possível, mas isso significa modificar o código original.
Feedback transforma drasticamente os testes do LLM
O valor central do tsma não é a indexação nem a medição de cobertura. É apontar com precisão as branches não cobertas, com números de linha.
Sem feedback:
"Escreva um teste para a função ListContracts"
→ LLM testa apenas o happy path
→ cobertura 60–70%
Com feedback:
"Escreva um teste para a função ListContracts"
→ cobertura 65% (11/17)
→ UNCOVERED:
line 41 — if params.Status != nil
line 44 — if params.BuildingId != nil
line 70 — if err != nil (CountSummary)
→ LLM adiciona testes que cobrem exatamente essas branches
→ cobertura 100%
Mesmo LLM. A única diferença é a presença do feedback. Três linhas com números de linha separam 60% de 100%.
Mesmo que o agente morra, o progresso é preservado
Agentes LLM caem. Limite de tokens, erro de rede, sessão desconectada. Não é possível processar 527 funções em uma única sessão.
tsma persiste o estado de progresso em .tsma/session.json.
$ tsma status
527 functions
PASS: 246 (46.7%)
DONE: 281 (53.3%)
TODO: 0 (0.0%)
Se o agente morre na função 200? Um novo agente executa tsma next e continua da 201. session.json é o checkpoint.
Vários agentes podem revezar sem conflito. A operação é atômica no nível da função.
A sessão é cache, os arquivos-fonte são a verdade
Um dos princípios de design do tsma: session é cache e os arquivos-fonte são o source of truth.
Se você apagar um arquivo de teste, mesmo que session.json registre PASS, a função volta a ser TODO. A sessão não se descola da realidade.
Princípio:
mesmo que session.json diga "PASS"
se não houver arquivo de teste → TODO
se o arquivo-fonte mudou → remesurar
Instruções para o agente LLM
O agente precisa de apenas 6 linhas:
1. Execute tsma next
2. Se TODO — leia a função e escreva o teste
3. Se o teste falhar — leia o erro e corrija o teste
4. Se branches não cobertas aparecerem — adicione testes que as cubram
5. Se PASS/DONE — a próxima função aparece automaticamente
6. Repita até aparecer "All functions complete!"
O único comando que o agente precisa é tsma next. O resto é imposto pela CLI.
O trem e os trilhos
Vibe coding é um trem. Rápido. Mas sem trilhos, descarrila.
Todas as ferramentas de codificação com IA estão focadas em tornar o trem mais rápido. Modelos maiores, agentes mais inteligentes, prompts melhores. Mas quanto mais rápido o trem, maior o estrago do descarrilamento.
tsma são os trilhos. O LLM gera os testes (Neural), e a CLI define “até aqui” (Symbolic Constraint). A criatividade do LLM permanece livre, mas a qualidade dos resultados é imposta pela máquina.
| Convencional | tsma | |
|---|---|---|
| Escrita de testes | Humano (lento) ou LLM (caótico) | LLM escreve, CLI verifica |
| Por onde começar? | Humano decide | CLI determina a ordem |
| Verificação de qualidade | Humano revisa | CLI mede cobertura |
| Feedback | Nenhum | Números de linha de branches não cobertas |
| Rastreamento de progresso | Nenhum | session.json automático |
O LLM gera livremente. Mas corre apenas sobre os trilhos do tsma next.
Linguagens suportadas
| Linguagem | Indexador | Runner de testes | Cobertura |
|---|---|---|---|
| Go | go/ast | go test | go test -coverprofile |
| TypeScript | regex | npx vitest / npx jest | c8 / istanbul |
| Python | regex | pytest | coverage.py |
Go usa parser AST para extração precisa de funções. TypeScript e Python são baseados em expressões regulares.
Arquivos gerados (*_gen.go, *.pb.go), arquivos de teste e vendor/node_modules são automaticamente excluídos da indexação.
Instalação e execução
make install
cd your-legacy-project
tsma next
Isso é tudo.
MIT License. github.com/park-jun-woo/tsma