Testes unitários: o que testar e o que evitar
O que separa testes que protegem comportamento real dos que só inflam coverage e viram fardo na hora de refatorar.
Você passa horas escrevendo testes para getters e setters. O coverage sobe para 95%. Você se sente produtivo. Aí chega uma mudança de requisito, você refatora três linhas de lógica de negócio, e a suíte inteira quebra — não porque o comportamento mudou, mas porque você testou implementação, não comportamento. Parabéns, você criou dívida de manutenção disfarçada de cobertura.
Esse artigo é sobre a decisão que separa testes que valem o custo dos que só viram fardo.
O que um teste unitário realmente protege
Um teste unitário não protege código. Ele protege comportamento. Essa distinção parece semântica, mas muda completamente o que você escreve.
Quando você testa código — "esse método retorna o valor da propriedade x" — você está testando uma linha que qualquer IDE verifica. Quando você testa comportamento — "dado esse pedido com desconto acumulado acima de R$500, o valor final deve incluir isenção de frete" — você está documentando uma regra de negócio que o compilador jamais vai verificar por você.
A pergunta certa antes de escrever qualquer teste: "se eu deletar essa linha de teste, o que fica sem garantia?" Se a resposta for "nada, o type checker já garante isso", o teste provavelmente não vale a pena.
O que vale testar
Lógica de domínio com múltiplos caminhos
Qualquer função que tem condicionais, casos especiais, limites ou combinações de estado é candidata. Cálculos de desconto, validações de formulário, transformações de dados, regras de autorização — esses são os lugares onde bugs de produção nascem.
def calcular_desconto(valor: float, cliente: Cliente) -> float:
if cliente.tipo == "premium" and valor >= 500:
return valor * 0.15
if cliente.tipo == "premium":
return valor * 0.10
if valor >= 1000:
return valor * 0.05
return 0.0
Essa função merece testes. Não um — vários. Um para cada combinação de tipo e valor que produz um resultado diferente. Se você só escrever um teste "feliz", está deixando quatro casos de borda sem garantia.
Funções puras com inputs não óbvios
Funções puras são as mais fáceis de testar e as mais valiosas de terem cobertura. Sem side effects, sem mocks necessários, sem setup complexo — você passa input, verifica output.
def formatar_cpf(cpf: str) -> str:
digits = re.sub(r'\D', '', cpf)
if len(digits) != 11:
raise ValueError(f"CPF inválido: {cpf}")
return f"{digits[:3]}.{digits[3:6]}.{digits[6:9]}-{digits[9:]}"
Aqui vale testar: CPF com máscara, sem máscara, com espaços, com letras misturadas, com 10 dígitos, com 12. Cada variante é um caso real que alguém vai passar pra essa função em produção.
Casos de borda explícitos
Limites numéricos, strings vazias, listas vazias, valores null/None, datas de virada de mês, valores negativos onde não se espera. Esses casos não aparecem nos testes "felizes", mas são onde a maioria dos bugs de produção acontece.
A regra é simples: se você se perguntou "o que acontece se...?" durante a implementação, isso é um teste.
Regressões documentadas
Quando um bug chega em produção, o primeiro passo antes de corrigir é escrever o teste que reproduz o bug. Só então corrigir. Esse teste garante que o bug nunca volte sem que alguém perceba — e serve de documentação do comportamento esperado.
O que não vale testar
Getters, setters e properties triviais
class Produto:
def __init__(self, nome: str, preco: float):
self._nome = nome
self._preco = preco
@property
def nome(self) -> str:
return self._nome # testar isso é desperdício
Não tem lógica aqui. O type checker verifica o tipo, o Python verifica o acesso. Um teste de getter só vai quebrar quando você renomear a property — e nesse momento o teste vai te dar informação zero sobre o que realmente quebrou.
Código de framework e bibliotecas externas
Se você está testando se o ORM salva corretamente no banco, você está testando o ORM, não o seu código. Confie que o SQLAlchemy, Django ORM, Prisma — seja o que for — funciona. Seus testes devem testar o que você escreveu em cima dessas ferramentas.
O mesmo vale para serialização simples: se você tem um campo nome e está testando que o JSON serializado contém "nome": "valor", você está testando a biblioteca de serialização.
Mocks em excesso
Quando um teste tem mais setup de mock do que lógica testada, isso é sinal de que você está testando a implementação, não o comportamento. Se você precisa mockar cinco dependências para testar uma função, considere se a função está bem estruturada — mas não considere isso um sinal de que o teste está sendo útil.
Mocks têm lugar: chamadas de rede, I/O de disco, serviços externos. Mas mockar um service que calcula desconto para testar outro service que usa esse desconto pode significar que você não está testando o fluxo real — e o bug vai estar exatamente na integração que você abstraiu.
Código que só existe por burocracia
Controllers que só repassam request para service, DTOs sem validação, adapters que só conversam formato. Se não tem lógica, não tem o que testar. Forçar coverage aqui é coverage theater.
A métrica de coverage é uma mentira (bem-intencionada)
100% de coverage não significa que a suíte é boa. Significa que cada linha foi executada pelo menos uma vez. Você pode ter coverage total e não testar nenhum caso de borda, nenhum caminho de erro, nenhuma combinação de estado não-trivial.
Coverage como número absoluto é útil para encontrar código morto e gaps óbvios. Como meta de qualidade, leva times a escrever testes vazios para atingir o percentual.
O número que importa não é coverage — é confiança. Você consegue fazer um refactor sem medo? Você consegue mudar a biblioteca de validação e saber que os testes vão pegar qualquer regressão de comportamento? Se sim, a suíte está fazendo o trabalho dela.
Testes de integração vs unitários: não é versus
Muito time gasta energia no debate errado. O ponto não é qual tipo é melhor — é usar cada um onde ele tem vantagem.
Testes unitários são rápidos e precisos: ótimos para lógica de domínio com muitos caminhos, onde você quer testar cada combinação em milissegundos. Testes de integração verificam que as partes funcionam juntas: ótimos para fluxos end-to-end, repositórios que falam com banco, handlers de API.
O erro é usar unitário onde integração seria mais adequado (mockar o banco quando o teste de repositório é o que importa) ou integração onde unitário seria mais rápido (subir todo o contexto da aplicação para testar uma função de formatação).
Perguntas frequentes
Quanto de coverage devo ter?
Depende do tipo de código. Lógica de domínio: alta — 80–90% faz sentido. Infrastructure e adapters: menor — código que só delega não precisa de cobertura agressiva. Coverage de projeto como número único é uma média que esconde onde você está bem e onde está mal.
Devo escrever testes antes ou depois do código?
TDD tem valor real em código de domínio complexo — escrever o teste antes te força a pensar na interface e nos casos de borda antes da implementação. Mas não é uma lei. Escrever o teste depois de ter entendido o problema também é legítimo. O que não é legítimo é não escrever teste nenhum porque "agora não tem tempo".
O que faço com código legado sem testes?
Não tente adicionar coverage retrospectivo em tudo. Priorize: quando for mudar uma função, escreva o teste que documenta o comportamento atual antes de mudar. Esse é o safety net que importa. Não vale horas testando código que não vai ser tocado.
Quando um teste que quebra é uma boa notícia?
Sempre que ele quebra por uma mudança de comportamento não intencional. O teste fez o trabalho dele. Quando ele quebra por uma mudança de implementação que não alterou comportamento — renomeou variável interna, refatorou estrutura — é sinal de que o teste estava testando detalhe de implementação, não contrato.
Testes bons são os que você quer escrever
O sinal mais claro de uma suíte saudável não é o número de testes ou o coverage — é a resistência da equipe a escrever mais. Se testes são vistos como burocracia, algo está errado: ou estão testando a coisa errada, ou o setup está difícil demais, ou os testes quebram por qualquer refactor trivial.
Para checar expressões regulares que entram em validators antes de testar, uso o Testador de Regex — útil para confirmar rapidamente se o pattern está correto antes de escrever o teste que depende dele.
Bons testes são baratos de escrever, rápidos de rodar, e só quebram quando o comportamento muda. Se os seus não são assim, a suíte não está te protegendo — está te desacelerando.
- 01 O que é DevOps além das ferramentas DevOps não é um pipeline nem um cargo. É responsabilidade compartilhada entre quem escreve código e quem coloca em produção — e por que a maioria dos times erra nisso.
- 02 Banco relacional vs NoSQL: como escolher Quando usar PostgreSQL e quando NoSQL faz sentido de verdade — tradeoffs reais de consistência, schema e escala, sem papo de marketing.