Todo mundo escreve teste. O problema é que boa parte deles quebra quando você renomeia um método, e fica quietinha quando o sistema realmente pifa. Teste que não dá confiança é só custo de manutenção com fantasia de qualidade.

Teste comportamento, não implementação
Se o seu teste sabe que o método chama repository.findById antes de cache.get, ele está acoplado ao como, não ao o quê. Refatorou a ordem interna sem mudar o resultado? O teste quebra. Isso é ruído.
Teste a interface pública e o resultado observável: entrada, saída, efeito colateral que importa. O detalhe interno é livre pra mudar. Regra prática: se dá pra reescrever a função inteira mantendo o contrato e o teste continua verde, você testou a coisa certa.
Um teste, um motivo para falhar
Quando um teste falha, você deve saber o que quebrou só lendo o nome. Se ele valida cinco coisas, a mensagem vira um enigma e você abre o debugger. Cada teste checa um comportamento.
Isso não quer dizer um assert por teste — é um conceito por teste. Validar que um objeto foi criado com nome e email corretos são dois asserts do mesmo comportamento; tudo bem. Já misturar "cria o usuário" com "dispara o email de boas-vindas" são dois testes.
Arrange, Act, Assert (e nada mais no meio)
A estrutura é simples: monte o cenário, execute a ação, verifique o resultado. Três blocos, nessa ordem, com uma linha em branco separando.
O erro comum é intercalar: um assert no meio do arrange, uma segunda ação depois da verificação. Aí o teste vira uma novela e ninguém entende onde começa o quê. Se você tem dois "act" no mesmo teste, provavelmente são dois testes.
Rápido e determinístico ou não serve
Suíte lenta ninguém roda. Se o teste unitário toca banco, rede ou relógio, ele nem é unitário — é integração disfarçada, e vai ficar lento e instável. Milissegundos por teste é o alvo.
Determinístico é inegociável. Nada de Random, now() ou ordem de execução importando. Teste que passa numa rodada e falha na outra — o famoso flaky — é pior que teste nenhum: ele treina o time a ignorar vermelho. E quando todo vermelho é ignorado, o vermelho de verdade passa batido.
Nome que explica o que quebrou
test1, testUser, deveFuncionar não dizem nada. Um bom nome descreve condição e resultado esperado: saque_com_saldo_insuficiente_lanca_erro. Quando esse teste fica vermelho no CI, você já sabe o estrago sem abrir o arquivo.
Pense no nome como a primeira linha do relatório de falha. Ele é lido muito mais vezes do que escrito.
Mock com moderação
Mock existe pra isolar dependência cara ou não-determinística — o gateway de pagamento, o envio de email. Não pra mockar tudo que se mexe.
Quando você mocka demais, o teste passa a verificar que seu código chamou os métodos que você achou que ele chamaria. Isso não é testar comportamento, é congelar a implementação atual — e amarra a refatoração de novo. Se um teste tem mais linha de setup de mock do que de lógica, desconfie: ou o design está acoplado demais, ou você está testando o mock, não o código.
Teste é código de primeira classe
A pior mentira que a gente conta é "é só teste, não precisa ficar bonito". Teste bagunçado apodrece igual código de produção, só que ninguém refatora porque "tá passando".
Teste tem duplicação? Extraia. Nome ruim? Renomeie. Setup gigante? É sinal de design ruim no código de produção, não desculpa pra ignorar. A suíte é a documentação executável do sistema — trate ela com o mesmo respeito.
Teste bom não é o que tem cobertura de 100%. É o que te deixa mudar o código sem medo e grita alto quando você quebra algo de verdade. O resto é teatro verde.


