Todos os artigos
45 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Dicas

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.

COVER · Dicas

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.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil