
Como comecei a escrever testes depois de um bug feio em produção
Trabalhei sem testes por meses. Um bug que voltou três vezes na mesma semana me convenceu de vez. Aqui está o que aprendi sobre testes que nenhum tutorial ensina direito.
Durante meses na POA Software eu entregava código sem teste automatizado.
Testava manualmente. Abria o Insomnia, fazia as chamadas, via se o resultado batia com o esperado. Funcionava. Era lento, mas funcionava.
Até um bug voltar três vezes na mesma semana.
O bug que apareceu, sumiu, e voltou
Era um bug de deduplicação de leads. Quando dois formulários chegavam com o mesmo email em menos de 1 segundo, os dois criavam registro no banco.
Corrigi uma vez. Testei manualmente. Parecia resolvido.
Na segunda-feira o cliente reportou de novo. Corrigi de novo. Testei de novo. Parecia resolvido.
Quarta-feira: de novo.
O problema é que meu teste manual não conseguia simular dois requests simultâneos. Testava um, depois o outro, com intervalo de alguns segundos. O bug só aparecia em alta concorrência.
Escrevi um teste automatizado que disparava duas requisições em paralelo. Bug confirmado. Corrigi. Rodei o teste. Passou. Não voltou mais.
O que teste automatizado faz que teste manual não consegue
Teste manual é linear. Você faz uma coisa, vê o resultado, faz outra.
Sistemas reais não são lineares. Têm concorrência, têm estado compartilhado, têm race conditions. Algumas classes de bug só aparecem sob condições que são difíceis ou impossíveis de reproduzir manualmente com consistência.
Além disso: teste manual não escala. Quando o sistema tem 20 endpoints, você consegue testar todos manualmente antes de cada deploy? Talvez. Com 200 endpoints? Não.
Teste automatizado roda tudo em segundos. Toda vez. Sem esquecer.
O que aprendi sobre o que testar
O erro que cometi quando comecei a escrever testes: testar implementação, não comportamento.
Teste ruim:
def test_criar_lead():
lead = Lead(nome="João", email="[email protected]")
assert lead.nome == "João"
assert lead.email == "[email protected]"
Isso testa que o construtor salva os campos. Não testa nada útil.
Teste melhor:
def test_nao_cria_lead_duplicado_com_mesmo_email():
criar_lead(email="[email protected]")
with pytest.raises(LeadDuplicadoError):
criar_lead(email="[email protected]")
def test_cria_lead_com_email_diferente():
criar_lead(email="[email protected]")
lead = criar_lead(email="[email protected]")
assert lead is not None
Testa comportamento de negócio. Se a implementação mudar internamente mas o comportamento se mantiver, o teste ainda passa. Se o comportamento mudar quando não deveria, o teste falha.
Tem teoria sobre pirâmide de testes. Na prática, o que aprendi: testes de integração para APIs (você quer garantir que o endpoint aceita os inputs certos, salva no banco, e retorna o output certo — isso é o que o cliente usa), testes unitários para lógica de negócio complexa (cálculo de desconto, algoritmo de deduplicação, transformação de dados com múltiplos casos de borda), e não testar o framework (Django sabe fazer query no banco — testa sua lógica, não a biblioteca).
O hábito que mudou tudo
Quando encontro um bug agora, antes de corrigir escrevo o teste que reproduz o bug.
# bug: leads duplicados chegam ao mesmo tempo
def test_deduplicacao_concorrente():
import threading
resultados = []
def criar():
try:
lead = criar_lead(email="[email protected]")
resultados.append('criado')
except LeadDuplicadoError:
resultados.append('duplicado')
threads = [threading.Thread(target=criar) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
assert resultados.count('criado') == 1
assert resultados.count('duplicado') == 4
Rode. Falha. Corrijo. Rode. Passa. O bug não vai voltar porque agora tem sentinela.
Essa semana: pega o último bug que você corrigiu. Consegue escrever um teste que teria detectado ele antes? Se sim, escreve agora. É o melhor investimento de 30 minutos que você pode fazer.