Antes de Escrever um Teste, Entenda Estes 4 Pilares!
Testes não devem verificar unidades de código. Em vez disso, devem verificar unidades de comportamento: algo que seja significativo para o domínio do problema. - Vladimir Khorikov
Falar sobre testes de unidade de novo? Pois é. Talvez você esteja pensando: "Sério mesmo? Mais um artigo dizendo que temos que escrever testes??"
Calma. Não é isso. Eu decidi trazer mais uma conversa sobre testes de unidade porque, depois de anos lidando com sistemas quebrando em produção, noites sem dormir e correções apressadas que geram novos problemas... eu vejo um valor em testes que vai muito além da teoria bonita dos livros.
Mas vamos ser sinceros:
Será que todo projeto precisa de testes? Até mesmo aqueles onde você precisa colocar um sistema no ar em menos de um mês para atender um cliente urgente? Será que testes são mesmo necessários ou eles só atrasam a entrega?
E mais: será que este artigo é uma tentativa de lacrar e dizer que você tem que escrever testes para cada linha de código? Não. Esse não é o objetivo.
O que eu quero aqui é compartilhar uma visão honesta — a visão de alguém que já sofreu (e muito) lidando com a falta de testes. Alguém que já viu pequenos bugs virarem grandes crises. Alguém que aprendeu que bons testes de unidade não são um luxo. Eles são uma forma de construir software que você pode confiar — mesmo quando o prazo é curto e a pressão é enorme.
Então, se você já teve que corrigir um bug crítico na sexta-feira à noite, ou se já sentiu aquele frio na barriga ao fazer uma alteração simples que podia quebrar tudo...
Esse artigo é pra você.
Bora conversar?
O que Indiana Jones pode nos ensinar sobre programar
Quando a gente entra na área de desenvolvimento, aprende um monte de fundamentos. Na faculdade, em cursos, ou mesmo ralando numa startup, ouvimos falar de tudo:
Lógica de programação,
Programação orientada a objetos,
Programação funcional,
Complexidade exponencial,
Estrutura de dados e algoritmos.
Com sorte, até ouvimos falar de testes de software, muitas universidades já falam ou tem uma matéria sobre esse tema. Mas me diz sinceramente:
Será que aprendemos de verdade a saber quando testar?
Será que aprendemos o que realmente precisa ser testado?
Será que alguém nos ensinou como pensar ao escrever um teste?
Normalmente não. A gente aprende as ferramentas. Aprende as técnicas.
Mas o instinto de saber proteger o que realmente importa — isso vem da experiência.
E, pra ilustrar essa jornada, eu queria trazer um exemplo um tanto inusitado: Indiana Jones. Sim, eu sei ele é um personagem de ficção. Mas, se a gente olhar de perto, Indy é um professor muito melhor do que parece. Ele nos ensina algo essencial: a experiência prática salva sua pele.
Indiana não é só alguém que "sabe de história antiga".
Ele é alguém que vive o campo de batalha. Que entende que, na vida real, mapas estão incompletos, armadilhas são invisíveis, e confiar apenas na teoria é pedir pra ser pego. No nosso caso, o "tesouro" que protegemos é o código e, mais ainda, as regras de negócio que ele representa.
Agora pense: será que às vezes nós programamos sem um mapa? Sem saber direito onde queremos chegar? Sem planejar o caminho, só confiando no impulso?
Talvez você pense: "Ah, mas eu tenho uma direção: é o que o cliente quer! O que o PO descreveu! O que os stakeholders pediram!"
Sim, é verdade. Eles querem a solução final funcionando. Querem que a entrega seja rápida. Querem valor agora.
E, pra isso, a gente corre, programa, coloca a aplicação no ar.
Parece que está tudo funcionando... até o primeiro input diferente.
Até o primeiro cenário que ninguém previu.
Porque uma solução feita correndo pode sobreviver ao "caminho feliz",
mas muitas vezes quebra no primeiro obstáculo inesperado.
Tá mas voltando a falar do Indiana, lembra daquela cena do filme, quando ele tenta trocar o Ídolo de Ouro por uma bolsa de areia?
Ele acha que planejou tudo. Que foi esperto.
Ele faz a troca... e, por um segundo, parece que tudo deu certo.
Mas logo depois, o chão desaba. Armadilhas disparam. E ele precisa correr pela vida.
É exatamente assim no nosso mundo:
Você entrega uma feature, acha que está tudo ok... e, no primeiro uso fora do esperado, tudo desmorona.
E é aí que os testes de unidade bem pensados entram. Eles são o nosso mapa.
Eles são a nossa forma de identificar armadilhas antes que seja tarde demais.
Mas pra que isso funcione de verdade, precisamos entender não apenas que testes existem, mas como e por que escrever testes que realmente protegem aquilo que importa.
Por que escrever testes de unidade? (De verdade, sem romantizar)
Testes de unidade não existem para cobrir 80% do código.
Não existem para enfeitar gráficos bonitos em dashboards.
Eles existem para algo muito mais importante: nos dar confiança.
Confiança de que aquilo que funciona hoje, vai continuar funcionando amanhã.
Confiança de que podemos refatorar, corrigir e evoluir um sistema sem medo de quebrar tudo sem perceber. Confiança de que vamos entregar valor ao cliente de forma sustentável.
Agora, vamos nos aprofundar mais nisso: faria sentido confiar apenas em cobertura de código? Confiar em cobertura de código é como confiar na quantidade de alarmes de incêndio instalados em um prédio — sem testar se eles realmente funcionam.
Você pode ter 100 alarmes espalhados, um em cada canto.
Parece seguro, certo?
Mas e se, na hora que precisar, nenhum deles disparar?
Cobertura de código é exatamente assim.
Ela mede onde o teste passou. Não se o comportamento do sistema está certo.
Não se o que foi testado faz sentido.
Não se o sistema está preparado para o inesperado.
Cobertura é fácil de manipular.
Se você quiser, consegue escrever testes que passam por todos os arquivos do seu projeto... sem validar absolutamente nada de útil.
Eu já falei sobre isso no artigo
Os Riscos e Limitações de Confiar Exclusivamente na Porcentagem de Cobertura de Código
É um equívoco comum entre alguns desenvolvedores que a refatoração do código, que resulta em um declínio no número total de linhas, inevitavelmente levará a uma melhoria automática na qualidade dos nossos testes e relatórios. No entanto, este raciocínio carece de fundamento lógico e não oferece benefícios reais para a qualidade do código ou a eficácia d…
Testes valiosos e eficazes não têm a cobertura como objetivo.
Cobertura alta é uma consequência natural de testes bem planejados, bem escritos e focados no comportamento observável do sistema.
Mas então... Por que empresas, clientes e até programadores ainda dão tanto valor pra cobertura? Aqui entra um ponto interessante: é da natureza humana.
Quando vemos um número "100%", o nosso cérebro libera dopamina — o neurotransmissor da sensação de recompensa.
É como se, inconscientemente, o nosso cérebro dissesse: "Está tudo perfeito! Pode ficar tranquilo!"
Esse efeito é tão forte que existe até um nome pra isso na psicologia: ilusão de completude.
A visão de um sistema com "100% de cobertura de testes" gera uma falsa sensação de segurança. É confortável. É bonito no relatório. Parece indicar qualidade.
Mas é só isso: uma aparência.
Sem profundidade.
Sem validar cenários críticos.
Sem proteger o sistema de verdade.
É por isso que, neste artigo, nós não vamos olhar para métricas vazias.
Nós vamos olhar para a essência do que realmente torna um teste de unidade bom:
A sua capacidade de proteger o código e as regras de negócio diante da realidade imprevisível do mundo real.
Testes como design: pensar antes de codar
A maioria dos desenvolvedores começa a programar com uma mentalidade muito comum:
"Entendi o que o sistema precisa fazer. Agora é só sair escrevendo código."
Só que existe um problema nisso:
Quando você simplesmente começa a codar sem pensar, o seu design nasce de forma desorganizada, inconsistente e, quase sempre, difícil de testar.
E aqui vem uma verdade importante:
Código difícil de testar é, quase sempre, código mal projetado.
Se você não consegue testar facilmente o que acabou de escrever, é porque as responsabilidades estão mal separadas.
É porque o acoplamento entre as partes é forte demais.
É porque sua lógica foi pensada apenas para "resolver" um problema imediato — e não para ser compreendida, mantida e evoluída.
E aqui entra a grande virada de mentalidade:
Testes não são só uma verificação do que você fez. Eles são uma ferramenta de design.
Pensar com testes antes de codar
Um desenvolvedor que escreve bons testes não começa pela implementação.
Ele começa pensando:
Quais comportamentos precisam existir?
O que é esperado acontecer em cada cenário?
O que deve ser garantido, mesmo quando as coisas dão errado?
Escrever testes antes ou ao menos rascunhar cenários de testes força você a visualizar o sistema de fora pra dentro. Força você a pensar em comportamentos, não em detalhes internos.
Isso pode parecer meio estranho no começo.
"Ué, mas como eu vou testar algo que ainda nem codifiquei?"
Mas pense em um engenheiro civil projetando uma ponte.
Antes de levantar qualquer pilar, ele simula a estrutura em ambientes controlados:
Será que ela aguenta o peso esperado?
Será que ela suporta a força do vento?
E se o solo ceder?
E se houver uma enchente ou um pequeno terremoto?
O engenheiro não constrói primeiro pra ver depois o que acontece.
Ele planeja sabendo que imprevistos existem.
É exatamente a mesma coisa com o código.
Quando você desenha seus cenários de testes primeiro, você já está:
Pensando em falhas e comportamentos inesperados.
Definindo o que é responsabilidade de cada parte do sistema.
Modelando uma estrutura que vai ser fácil de validar e difícil de quebrar por acidente.
Você não programa às cegas.
Você programa com propósito.
E mais: testar melhora até mesmo a escrita do código
Quando você se obriga a pensar em como algo será testado:
Você evita dependências desnecessárias.
Você cria métodos e classes menores, mais coesas.
Você respeita melhor princípios como SRP (Single Responsibility Principle).
Você naturalmente separa lógica de infraestrutura (o famoso "não acople regra de negócio com API ou banco direto").
No fim, escrever código fácil de testar é escrever código fácil de entender, de manter e de evoluir.
Se é difícil de testar, é difícil de manter.
E se é difícil de manter, o futuro daquele sistema já está comprometido — mesmo que hoje ele funcione.
TDD, Ética e a Realidade que Muitos Fingem Não Ver
Depois de tudo que falamos sobre pensar antes de codar, muita gente provavelmente lembrou do famoso TDD — Test-Driven Development.
E sim, faz sentido. TDD é justamente a prática que formaliza essa ideia de pensar em testes antes da implementação.
Mas sejamos bem realistas aqui:
TDD ainda é mal compreendido.
E, pior, é alvo de uma arrogância absurda.
Já perdi minutos preciosos da minha vida tentando argumentar sobre isso em fóruns de discussão. Já tive conversas acaloradas com homens feitos, barbados, com títulos bonitos como Microsoft Most Valuable Professional (MVP), que simplesmente desprezam a opinião de outros e experiência de outros.
Concordo que nem tudo é vantagem imediata. TDD não faz sentido para tudo.
Se você está construindo uma PoC (Prova de Conceito), ou tentando validar uma ideia rapidamente, ou se o time ainda é muito inexperiente, talvez o ciclo de TDD só trave mais do que ajude.
Mas aqui vai uma verdade que poucos têm coragem de dizer:
Tem "senior" por aí que não sabe escrever sequer uma linha de teste de unidade.
"Seniors" de cargo, mas não de atitude. Seniors que, na prática, jogam a responsabilidade nas costas de Juniors e Plenos, e ainda querem ser "chefes" de fiscalização, monitorando cada passo dos outros — sem serem capazes de dar exemplo.
Esses são a verdadeira escória da engenharia de software.
Não constroem.
Não ensinam com paciência os iniciantes.
Não protegem os projetos que tocam.
Vivem repetindo que "boas práticas são perda de tempo", ou que "o importante é só entregar, depois vemos como melhorar". E, no fundo, o que estão dizendo é:
"Depois que eu entregar o código, o problema é de quem ficar para lidar com a bagunça."
Esses "profissionais" — e aqui eu uso a palavra com pesar — não estão formando a próxima geração. Estão sabotando.
TDD, SOLID, boas práticas = Ética profissional
Quando você ouve falar de TDD, SOLID, princípios de design, testes bem escritos, não pense em "frescura". Não pense em "encheção de linguiça".
Pense em ética.
Assim como esperamos de um médico que ele lave as mãos antes de uma cirurgia,
assim como esperamos que um engenheiro civil respeite normas de segurança,
espera-se que nós, desenvolvedores, sejamos éticos com o que entregamos.
As pessoas que usam seu software merecem isso. As empresas que investem seu dinheiro merecem isso. Você, como profissional, merece ter orgulho do que constrói.
Uma pergunta direta pra você, caro leitor:
Como podemos construir software seguro, confiável, estável — com funcionalidades que as pessoas possam depender — se nós mesmos não testamos de forma ética e profissional?
A resposta é simples: Não podemos. Não adianta colocar no LinkedIn que você é "apaixonado por tecnologia" se você não respeita quem vai usar o seu produto. Paixão sem responsabilidade é só ego disfarçado. Testar direito é um ato de respeito. De ética. De profissionalismo.
"Mas e os prazos?" — A verdade que precisei encarar…
Talvez você esteja pensando agora:
"Ah, mas você está exagerando! Eu sempre tento escrever testes, mas meu gestor me impede!" "Eu queria codar melhor, mas não dá tempo!"
Eu entendo. De verdade. Sabe por quê? Porque eu mesmo já caí nessas desculpas.
E sim, existe um fundo de verdade nelas. Os prazos realmente não são flexíveis. O mercado cobra. Os clientes cobram. A pressão é real.
Mas existe uma verdade ainda maior que eu precisei encarar — e que talvez você também precise:
Mesmo dentro de prazos apertados, cabe a nós fazer o possível para entregar confiança e qualidade.
Então você está dizendo que o problema sou eu?
Não. O problema não é você. O problema é:
Quanto você está se dedicando para fazer o seu melhor dentro do que é possível?
"Ah, Rafael, isso parece papo de coach..."
Não, não é. É só uma questão simples de realidade.
Essa pergunta não é para te pressionar. Não é sobre te colocar culpa.
E definitivamente não é sobre te comparar com outras pessoas.
É sobre você olhar para si mesmo de forma honesta e se perguntar:
Eu estou plantando algo hoje que vai me ajudar a crescer e me tornar alguém melhor no futuro?
Você faz isso não porque alguém está te cobrando a cada segundo,
mas porque você sabe que é o certo. Porque você sabe que os frutos vêm com o tempo. Aquelas horas em que você está sentado codando não são para:
Bater ticket de entrega,
Agradar gestor,
Passar o tempo até dar a hora de ir embora.
São horas que moldam sua experiência. São horas que treinam seu raciocínio lógico, sua capacidade de análise, sua habilidade de construir sistemas melhores. São sementes invisíveis que você planta e que, lá na frente, se transformam em conhecimento, em maturidade técnica, em segurança profissional.
E se o ambiente não for bom?
Se a empresa onde você está hoje não te agrada, se você não gosta da gestão,
se você sente que poderia estar em um lugar melhor...
Tudo bem. Isso não invalida seu esforço. Você pode e deve buscar novas oportunidades, novos horizontes, lugares que respeitem seu valor.
Mas enquanto estiver nesse contexto atual, aproveite para transformar até as situações ruins em oportunidades:
Oportunidades de se tornar mais resiliente,
De praticar a excelência mesmo sem reconhecimento imediato,
De se preparar melhor para o próximo degrau da sua carreira.
O profissional que floresce não é o que reclama das condições é o que cresce apesar delas.
Cada linha de código que você escreve com responsabilidade hoje, mesmo nas piores circunstâncias, é um investimento que só você pode fazer por você mesmo.
Seja pela sua carreira, pelo seu orgulho pessoal, pela sua evolução como pessoa e como engenheiro.
E nisso, os testes de unidade, o cuidado com a qualidade, a busca pelo melhor que você pode fazer — não são obrigações impostas. São expressões da pessoa e do profissional que você está se tornando.
E onde entra o TDD nessa história?
TDD — Test-Driven Development — não é sobre seguir regra de moda.
Não é sobre "usar porque o livro manda".
TDD é sobre mudar a forma como pensamos antes de sair digitando código.
É sobre parar e perguntar:
O que realmente precisa acontecer aqui?
Quais comportamentos são esperados?
Como eu sei que isso está funcionando?
E se algo sair do padrão? Como meu sistema deve se comportar?
Pensar antes de escrever a primeira linha pode abrir a sua mente.
Pode te fazer enxergar além da feature imediata. Pode revelar armadilhas, incoerências ou problemas de design antes que virem dores reais em produção.
Não é questão de romantismo. É questão de construir com responsabilidade.
De transformar prazos apertados em oportunidades de trabalhar melhor, não apenas mais rápido.
A visão pragmática de Vladimir Khorikov: uma luz no fim do túnel
O que o Vladimir faz no seu livro "Unit Testing Principles, Practices, and Patterns" é mais do que ensinar a testar. Ele mostra como pensar testes de um jeito realista, que respeita o dia a dia dos desenvolvedores — com prazos apertados, mudanças constantes e sistemas cada vez mais complexos.
Enquanto muita gente prega fórmulas prontas ou dogmas cegos, Vladimir trouxe algo que parecia estar faltando no meio de tanta teoria:
Bom senso. Praticidade. Conexão com a realidade.
Ele não propôs "testar tudo o que se move". Ele propôs construir testes que sejam verdadeiros aliados do código.
E pra isso, ele identificou quatro pilares que sustentam um bom teste de unidade.
Vamos conversar um pouco mais sobre cada um, de um jeito que qualquer pessoa possa visualizar:
🛡️ Proteção contra regressões
Pense num castelo com muralhas. As muralhas existem para proteger quem está lá dentro. Se alguém derruba uma parte da muralha e ninguém percebe... no dia seguinte, o inimigo entra sem resistência.
É isso que acontece com um sistema sem bons testes:
Você muda algo hoje, acha que não afetou nada... mas lá na frente, vem um bug que quebra tudo.
Testes bons são como sentinelas atentas.
Se alguma mudança acidental quebrar uma regra importante do sistema, eles vão soar o alarme imediatamente.
🔄 Resistência à refatoração
Agora imagine que você quer reformar seu castelo: reforçar as muralhas, pintar a torre, trocar o portão.
Se, a cada martelada, a estrutura toda desmoronasse, seria um inferno, não?
Testes frágeis são assim:
Você muda a estrutura interna do código (pra melhorar) e, mesmo sem mudar o comportamento do sistema, os testes quebram.
Bons testes são flexíveis.
Eles se preocupam com o que o sistema faz, não como ele faz.
Se a funcionalidade continua correta, os testes nem deveriam notar que a implementação interna mudou.
⚡ Feedback rápido
Agora imagine que toda vez que você reforçasse uma muralha, tivesse que esperar 6 horas pra saber se ela aguenta vento ou não.
Insuportável, né?
É o que acontece quando seus testes demoram demais para rodar:
Você perde agilidade, perde foco, desanima de testar.
Bons testes te dão respostas em poucos segundos.
Você muda o código e já sabe quase instantaneamente se está no caminho certo ou se quebrou algo.
Testes rápidos mantêm seu fluxo de trabalho leve e produtivo.
🛠️ Manutenibilidade
Por fim, imagine que, para trocar uma simples porta do castelo, você tivesse que demolir metade da muralha.
Seria ridículo.
Testes difíceis de manter fazem exatamente isso: qualquer pequena mudança no sistema vira um pesadelo de ajustes nos testes.
Bons testes são simples, claros e fáceis de adaptar.
Quando você olha pra eles, entende de cara o que estão validando.
E quando precisa ajustá-los, faz isso com facilidade.
Testes manuteníveis acompanham a evolução natural do sistema sem virar uma carga extra.
Por que esses pilares são tão importantes?
Esses quatro pilares, juntos, não são apenas "boas práticas".
Eles representam uma visão pragmática e madura de qualidade.
A visão de Vladimir Khorikov é uma luz no fim do túnel porque:
Ela nos ensina a usar testes como aliados, não como obstáculos.
Ela mostra que qualidade de testes não é sobre quantidade, é sobre proteção real.
Ela nos lembra que escrever testes é parte da nossa ética profissional: proteger o que construímos, dar orgulho ao nosso trabalho e respeito aos usuários.
Esses pilares são o caminho para trabalhar melhor — de forma mais segura, mais confiável e mais tranquila.
🛡️ Proteção contra regressões: nossa primeira linha de defesa
Vamos começar destrinchando o primeiro atributo de um bom teste unitário: proteção contra regressões.
No Capítulo 1 do livro, Vladimir Khorikov já nos lembra:
Uma regressão é um bug.
Um recurso que funcionava corretamente para de funcionar depois de uma modificação no código.
E aqui vem uma verdade desconfortável, mas essencial:
Quanto mais funcionalidades você adiciona, maior o risco de quebrar algo antigo sem perceber.
Não é porque você é descuidado.
Não é porque seu time é ruim.
É simplesmente porque código não é um ativo estático.
Código é um passivo.
Cada linha que você adiciona à base de código aumenta a superfície de risco.
Quanto maior a base, maior a chance de problemas surgirem.
Sem uma proteção forte contra regressões, o crescimento de um projeto vira um ciclo de dor:
Mais funcionalidades → Mais bugs escondidos → Mais retrabalho → Mais medo de mexer no sistema.
Por isso, testes de unidade que protegem contra regressões não são opcionais.
Eles são essenciais para a saúde de longo prazo de qualquer sistema.
Como avaliar a proteção contra regressões de um teste?
Para saber se um teste realmente protege contra regressões, você precisa pensar em três fatores:
Quantidade de código que é executado durante o teste.
Quanto mais código relevante você testa, maior a chance de detectar problemas.Complexidade do código testado.
Se o código envolve regras de negócio mais complexas (e não só "copiar valores"), ele merece atenção especial.Importância da regra para o domínio do sistema.
Não é tudo que merece o mesmo nível de proteção. Regra de negócio crítica = prioridade alta. Código trivial (getter/setter de uma propriedade simples)? Nem sempre compensa testar.
Protegendo uma regra de voucher
Imagine que você trabalha num e-commerce que permite aplicar vouchers de desconto nas compras.
Existe uma regra importante:
Se o voucher for expirado ou inválido, ele não pode gerar desconto.
Isso é uma regra de negócio crítica.
Se ela quebrar, a loja pode perder dinheiro ou prejudicar a experiência do cliente.
Agora imagine o seguinte cenário:
Você implementou a regra corretamente hoje.
Daqui a três meses, outro desenvolvedor mexe na lógica de carrinho de compras.
Sem perceber, ele altera o fluxo de validação de voucher.
public class Voucher {
private boolean valid;
private boolean expired;
private double discountAmount;
public Voucher(boolean valid, boolean expired, double discountAmount) {
this.valid = valid;
this.expired = expired;
this.discountAmount = discountAmount;
}
public double applyDiscount(double totalAmount) {
// Regra de negócio: Se o voucher for inválido ou expirado, não aplicar desconto
if (!valid || expired) {
return totalAmount;
}
return totalAmount - discountAmount;
}
}
Se você não tiver um teste de unidade protegendo essa regra, como vai saber que ela foi quebrada?
Resposta: Só vai descobrir quando os usuários começarem a aplicar cupons inválidos e levarem descontos indevidos. Tarde demais.
Como um programador pode quebrar a validação de voucher sem perceber
Você pode estar se perguntando agora:
“Mas como alguém conseguiria quebrar uma regra tão simples assim... sem nem perceber?”
A verdade é que isso acontece com muito mais frequência do que imaginamos.
Deixa eu te dar um exemplo bem realista:
Alguns meses depois da primeira versão do sistema, o projeto cresceu. Agora existem diferentes tipos de vouchers — cupons promocionais, de fidelidade, de indicação, e por aí vai.
O programador chamado para implementar essa nova lógica (talvez com pouco contexto da regra original) olha para o método applyDiscount
e pensa:
“Hmm... essa validação aqui poderia ser mais robusta. Vou melhorar isso pra garantir que só cupons bons passem!”
Então ele reescreve o método assim:
public double applyDiscount(double totalAmount) {
if (discountAmount <= 0 && !valid && expired) {
return totalAmount;
}
return totalAmount - discountAmount;
}
À primeira vista, parece que ele reforçou a proteção. Mas tem um erro grave aqui. Olhe com atenção.
⚠️ Qual foi o problema?
Ele trocou o operador ||
(OU) por &&
(E lógico).
Isso muda completamente o comportamento da validação!
O que o negócio realmente espera?
Vamos deixar isso bem claro:
O desconto só deve ser aplicado se — e apenas se —:
O voucher for válido (
valid == true
),Ele não estiver expirado (
expired == false
),E o valor do desconto for positivo (
discountAmount > 0
).
Se qualquer uma dessas condições falhar, o sistema deve negar o desconto.
O sistema deve ser desconfiado, nunca permissivo.
E o que o programador fez?
Ao usar &&
para negar o desconto, ele criou uma regra que só bloqueia o desconto se TODAS as condições forem ruins ao mesmo tempo:
O desconto for zero ou negativo,
E o voucher for inválido,
E ele estiver expirado.
Isso significa que basta uma das condições ser boa, e o desconto será aplicado!
Veja só:
Se o voucher estiver expirado, mas for válido e com desconto positivo → desconto aplicado. ❌
Se o desconto for zero, mas o cupom for válido e não expirado → desconto aplicado. ❌
Quando estamos lidando com regras de negócio, o problema não é só quando o código quebra.
Às vezes, o código funciona... mas do jeito errado.
É o tipo de bug silencioso que só aparece quando um cliente usa o sistema de verdade.1
🚨 Por que isso é tão crítico?
Se o seu teste de unidade estivesse protegendo corretamente essa regra — testando o comportamento observável e não a implementação — esse erro de lógica seria detectado na hora.
O sistema não deixaria passar um voucher inválido aplicando desconto por engano.
Sem testes, esse tipo de erro silencioso vai direto pra produção...
e só é percebido depois de causar prejuízo financeiro, problemas de confiança e retrabalho emergencial.
Mas isso não se trata de má fé. Não se trata de incompetência. Se trata da realidade da engenharia de software: sistemas mudam, pessoas mudam, o entendimento do sistema se perde com o tempo.
É por isso que testes que protegem regras de negócio críticas são indispensáveis:
Eles são os guardiões invisíveis do sistema, garantindo que valores essenciais não sejam esquecidos ou quebrados sem querer.
Como identificar o que merece proteção?
Aqui vai um filtro simples:
A regra está ligada a comportamento importante para o negócio?
(ex.: cálculo de preços, validação de pagamentos, aplicação de descontos, envio de pedidos?)Existe uma possibilidade razoável de alguém quebrar isso no futuro ao mexer no sistema?
(ex.: lógicas que dependem de múltiplos fatores, estados ou integrações.)O impacto de quebrar essa regra seria alto?
(ex.: perda de receita, impacto em clientes, problemas legais.)
Se a resposta para qualquer uma dessas perguntas for "sim",
essa regra merece ser protegida com testes unitários fortes.
Então para deixar claro!
Bons testes de regressão:
Executam códigos relevantes e críticos.
Validam o comportamento esperado com assertivas claras.
Servem como alarmes contra mudanças incorretas.
Testar propriedades triviais (tipo getId()
, setId()
) geralmente não vale a pena.
Proteja o que importa. Proteja o que, se quebrar, vai custar caro.
🔄 Resistência à refatoração: testando o que importa de verdade
Já falamos brevemente sobre resistência à refatoração antes, mas agora é hora de mergulhar fundo nesse pilar.
A resistência à refatoração é o que faz o seu teste ser um aliado da evolução do sistema — e não uma âncora que trava seu projeto.
Em outras palavras:
Bons testes te deixam mudar o código com liberdade, desde que o comportamento final do sistema continue correto.
O que isso quer dizer na prática?
Quer dizer que os testes não podem depender da forma como o sistema é implementado. Eles têm que se preocupar apenas com o comportamento observável.
O que é comportamento observável?
Comportamento observável é o que o sistema mostra para o mundo exterior.
É o que um consumidor do seu código enxerga:
Inputs e outputs.
Efeitos visíveis.
Mudanças de estado expostas através de comportamentos.
Não importa quantos métodos privados você criou.
Não importa se você mudou o algoritmo interno.
Não importa se você refatorou para usar uma estratégia mais eficiente.
Se a entrada e a saída continuam iguais, o sistema se comporta igual.
E é isso que os testes deveriam validar.
Vou tentar ilustrar isso de forma clara:
Imagine que você está construindo uma funcionalidade simples:
somar dois números para compor o valor total de um pedido (por exemplo, preço de produto + valor do frete).
No começo, a implementação é simples e direta:
public double calculateTotal(double productPrice, double shippingFee) {
return productPrice + shippingFee;
}
E o teste para validar esse comportamento seria:
@Test
void shouldCalculateTotalCorrectly() {
double total = calculateTotal(100.0, 15.0);
assertEquals(115.0, total, 0.001);
}
Agora imagine que, depois de algum tempo, o time decide refatorar para usar uma biblioteca externa que tem uma função de soma com controle de arredondamento, logs internos ou suporte a BigDecimal.
O código refatorado agora usa essa biblioteca:
import com.external.library.MathHelper;
public double calculateTotal(double productPrice, double shippingFee) {
return MathHelper.add(productPrice, shippingFee);
}
Mudou alguma coisa para quem usa o sistema?
A entrada (
productPrice
eshippingFee
) continua igual? ✅ Sim.A saída esperada (o valor total) continua igual? ✅ Sim.
O comportamento observável mudou? ❌ Não.
Então o teste deve continuar passando normalmente.
🚨 O que aconteceria com testes frágeis?
Se o seu teste estivesse dependente da forma como a soma era feita (ex.: verificando se o operador +
foi usado diretamente), o teste quebraria — mesmo que o sistema continuasse funcionando corretamente.
Ou seja, o teste teria se acoplado à implementação interna, e não ao comportamento observável que realmente interessa.
Se o resultado para quem consome seu sistema não muda, o teste deve continuar verde. Testes são contratos de comportamento, não de implementação.
⚡ Feedback rápido: seu sistema precisa te responder na velocidade certa
Um dos pilares fundamentais de testes de unidade é o feedback rápido.
E quando falamos "rápido", é rápido mesmo: segundos.
No máximo, alguns segundos.
Testes de unidade precisam rodar tão rápido que pareçam instantâneos.
Se um teste demora minutos pra rodar, ele vira um fardo psicológico.
E acredite:
Testes lentos matam o hábito de testar.
Como é a realidade com testes lentos?
Imagine o cenário:
Você faz uma pequena alteração em uma classe. Você quer rodar os testes para ter certeza que não quebrou nada.
Mas aí lembra:
"Rodar todos os testes vai demorar cinco minutos..."
"Ah, é só uma mudança pequena, acho que tá certo..."
"Vou rodar depois..."
E pronto. Você acabou de pular o teste. O bug entrou. A confiança foi quebrada. Isso é real, é humano, e acontece todos os dias.
Não importa o framework. Não importa se é JUnit, NUnit, Jest, Pytest, xUnit.
Testes de unidade precisam ser rápidos, consistentes e confiáveis.
Se eles são lentos, você vai naturalmente começar a:
Ignorar testes.
Testar "só de vez em quando".
Confiar no feeling ("parece que não quebrou nada...").
Depender do CI para descobrir problemas (tarde demais).
Em um projeto saudável:
Você faz um
git commit
,Roda toda a suíte de testes local,
E em poucos segundos você sabe:
✅ Tudo certo, ou
❌ Alguma coisa quebrou.
Esse tipo de feedback imediato te dá liberdade para:
Refatorar sem medo.
Evoluir o sistema com segurança.
Se concentrar na lógica do negócio, e não no medo de quebrar algo.
🎯 Por que é tão crítico?
Quanto maior o tempo de feedback, menor a chance de você querer testar.
Quanto mais rápido o feedback, mais natural e seguro se torna testar continuamente.
É como praticar esportes:
Se você treina e vê resultados rápidos, você se motiva.
Se demora meses pra ver uma pequena evolução, você desanima.
Com testes é a mesma coisa. Feedback rápido alimenta a cultura de qualidade.
Feedback lento destrói a cultura de testes.
O que normalmente onera o tempo dos meus testes?🐢
Se você chegou até aqui, pode estar se perguntando:
"Tá bom, Rafael, entendi que testes precisam ser rápidos...
Mas por que meus testes às vezes demoram tanto pra rodar?"
Ótima pergunta. E a resposta está geralmente ligada a três problemas muito comuns que vemos em projetos no mundo real:
1. Dependência de recursos externos
Um dos maiores vilões da lentidão nos testes é depender de recursos que não estão dentro da memória local — como:
Banco de dados,
Arquivos no disco,
Servidores de API,
Sistemas de terceiros.
Sempre que o seu teste depende de "sair da aplicação" para buscar ou salvar alguma coisa, você introduz variáveis externas (latência, falhas de rede, processamento externo).
E isso mata a velocidade.
Além de lento, o teste se torna instável:
Às vezes passa, às vezes falha, dependendo do ambiente.
2. Testes que fazem operações pesadas ou em lote
Outro problema comum é que alguns testes, mesmo de unidade, começam a executar operações em grande volume:
Processam centenas ou milhares de registros,
Fazem cálculos pesados desnecessariamente,
Rodam loops demorados.
Mas lembre:
Teste de unidade é para validar um comportamento pequeno, isolado.
Se seu teste precisa "fazer mil coisas" para validar algo simples,
provavelmente o código de produção também está com responsabilidades demais — e aí o teste sofre junto.
Testes lentos geralmente revelam código mal estruturado.
3. Testes que dependem de setup complexo
E finalmente, um dos piores assassinos do tempo de execução:
Testes que precisam montar um ambiente inteiro antes de começar.
Várias instâncias de objetos,
Muitas dependências para serem mockadas manualmente,
Dependência de contexto de framework (ex: carregar Spring Boot inteiro para testar uma classe).
Quanto mais pesado for o setup inicial, mais doloroso será rodar o teste.
E o mais perigoso:
O programador começa a "deixar para rodar os testes depois" porque sabe que vai tomar chá de cadeira esperando.
🛠️ Manutenibilidade: testes que vivem junto com o sistema
Até aqui, falamos sobre como testes protegem o sistema contra regressões, dão liberdade para refatoração e precisam ser rápidos.
Agora, vamos falar sobre algo igualmente crucial: manutenção de testes.
Porque sistemas mudam.
Funcionalidades evoluem.
Regras de negócio são estendidas.
Requisitos mudam conforme o produto amadurece.
E se o sistema muda, os testes vão mudar também.
Isso é normal. Isso é esperado.
A pergunta é:
Você vai conseguir manter seus testes com facilidade? Ou cada mudança vai ser uma batalha?
O que o Vladimir Khorikov destaca sobre manutenibilidade?
No livro Unit Testing: Principles, Practices, and Patterns, Vladimir aponta dois fatores principais para avaliar a manutenibilidade dos testes:
1. Facilidade de entender o teste
Quanto mais fácil for ler e entender o que o teste está validando, melhor.
Testes pequenos são mais legíveis.
Testes focados em um único comportamento são mais fáceis de modificar.
Testes que usam boas práticas de escrita (bons nomes, boas descrições) facilitam a vida.
Não é sobre economizar linha de código de forma artificial. É sobre escrever testes limpos, expressivos, que fazem sentido sem precisar adivinhar o que estão testando.
Testes são tão importantes quanto o código de produção.
Não corte caminho. Não trate testes como "segunda classe".
Se você escrever testes mal feitos hoje, você mesmo vai sofrer amanhã.
2. Facilidade de rodar o teste
Testes de unidade não devem depender de ambientes externos.
Se seu teste precisa de banco de dados pra rodar, já é um peso a mais.
Se seu teste precisa de serviço externo ativo, já é outra preocupação.
Se pra rodar os testes você depende de estar conectado em VPN, ou com Docker subindo containers, isso atrasa todo o fluxo.
Cada dependência externa é uma dor de cabeça potencial na manutenção.
Quanto mais isolado for o teste, mais confiável e fácil de manter ele será.
E o que isso significa pra nós, desenvolvedores?
Significa que:
Precisamos escrever testes que falam por si mesmos.
Quando você abrir um teste depois de seis meses, ele precisa "contar a história" do que está validando.
Precisamos escrever testes pequenos e focados.
Um teste = um comportamento validado.
Precisamos evitar dependências externas a todo custo em testes de unidade.
Mocks, fakes e stubs são seus amigos aqui.
Bancos, redes, APIs externas? Só em testes de integração, onde isso faz sentido.
Precisamos cuidar dos testes como cuidamos do código de produção.
Código ruim gera dívida técnica.
Teste ruim gera dívida técnica silenciosa — e, pior ainda, destrói sua confiança nos testes.
Testes como documentação viva do sistema
Quando pensamos em testes de unidade, a primeira imagem que vem à cabeça geralmente é: "Eu preciso garantir que o código funcione."
Sim, isso é verdade. Mas existe algo ainda mais poderoso que bons testes podem nos oferecer:
Testes de unidade podem e devem servir como documentação viva do sistema.
Como assim documentação viva?
Testes de unidade bem escritos contam uma história.
Eles explicam, com exemplos reais, como o sistema deve se comportar em diferentes situações.
Eles respondem perguntas fundamentais que surgem no dia a dia:
O que acontece se o usuário aplicar um voucher expirado?
Qual a regra para calcular o desconto máximo permitido?
O que o sistema deve fazer se o estoque do produto for igual 0 ou for igual a 1?
Um bom teste de unidade é como uma conversa entre você e o sistema, onde o sistema responde:
"Se o voucher estiver expirado, o desconto não é aplicado."
"Se o estoque for negativo, a venda não é finalizada."
E o mais interessante?
Essa "documentação" é sempre atualizada automaticamente junto com o código.
Se a regra de negócio mudar, o teste precisa mudar também. Se o teste falha, é sinal de que o que está documentado já não reflete mais a realidade. Por isso chamamos de documentação viva.
Mas para isso ser verdade, precisamos respeitar a manutenibilidade
Se os testes forem difíceis de entender...
Cheios de gambiarras,
Sem clareza no que estão validando,
Lotados de mocks inúteis,
Cheios de nomes genéricos (
testSomething
,testCase1
,shouldWorkProperly
),
...então eles não documentam nada.
Eles só confundem.
Testes de unidade só podem ser considerados documentação se forem fáceis de ler, fáceis de entender e fáceis de confiar.
É aqui que o pilar da manutenibilidade se conecta com a ideia de documentação viva.
Display Names (ou boas descrições)
Uma prática simples, mas extremamente poderosa, é usar Display Names (ou boas descrições) para deixar a intenção do teste explícita. Em vez de:
@Test void testVoucher() { // ... }
Use:
@DisplayName("Não deve aplicar desconto se o voucher estiver expirado") @Test void shouldNotApplyDiscount_WhenVoucherIsExpired() { // ... }
Ou até melhor:
@DisplayName("Aplicação de desconto: não deve aplicar se o voucher for expirado ou inválido") @Test void shouldNotApplyDiscount_WhenVoucherIsInvalidOrExpired() { // ... }
Assim, só de ler o nome do teste, você já entende a regra de negócio. Sem precisar abrir o método. Sem precisar ler o corpo. Sem precisar adivinhar.
Teste bom é teste que ensina
O melhor teste não é aquele que apenas valida, mas aquele que também ensina o que o sistema deve fazer.
Testes claros, focados, bem nomeados são ativos valiosos.
Eles ajudam você, ajudam seu time, ajudam o Rafael do futuro que vai dar manutenção nesse código daqui a 1 ano.
Testes de unidade deixam de ser apenas uma verificação técnica para se tornarem um mapa vivo do que o sistema realmente é.
E, num mundo em que documentações ficam obsoletas em meses,
testes claros são a melhor documentação que você pode ter.
Conclusão
Se você chegou até aqui, já percebeu que este artigo não foi apenas sobre testes de unidade. Foi sobre responsabilidade. Sobre consciência. Sobre ética profissional.
Começamos com um desabafo sincero:
Sobre o quanto a nossa área ainda sofre com maus exemplos, sobre como é fácil cair na armadilha do imediatismo, da pressa, da negligência.
Falamos sobre o peso que assumimos como desenvolvedores:
Não apenas construir software que funciona — mas construir software que sobrevive.
E aí mergulhamos no coração dos testes de unidade:
não como um ritual chato, não como um número mágico de cobertura,
mas como um compromisso sério com a qualidade do que entregamos.
O que conversamos pelo caminho:
Que testes de unidade não são apenas sobre validar código — são sobre proteger comportamentos que importam.
Que testes devem te libertar para melhorar o sistema, e não te prender ao medo de mexer.
Que testes precisam ser rápidos, para se encaixar no seu fluxo natural de trabalho.
Que testes devem ser manuteníveis, fáceis de ler, entender e evoluir — porque o sistema vai mudar, e seus testes precisam acompanhar.
Que testes podem — e devem — ser a documentação viva do seu sistema, contando com clareza as regras que movem o seu software.
E os 4 pilares que você deve levar consigo, sempre:
Proteção contra regressões — Testes devem gritar quando algo essencial quebrar.
Resistência à refatoração — Testes devem focar no comportamento, não na implementação.
Feedback rápido — Testes devem responder em segundos, para você confiar neles.
Manutenibilidade — Testes devem ser fáceis de entender, fáceis de rodar, fáceis de adaptar.
Se hoje você precisasse confiar cegamente nos testes do seu sistema, você confiaria?
Se a resposta for "não", "depende" ou até "talvez"... está tudo bem.
Este artigo não foi para te julgar. Foi para te convidar a construir não apenas software que "entrega valor rápido", mas software que orgulha, que sustenta negócios, que protege as pessoas que dependem dele. Esse é o caminho da engenharia de software de verdade. E tudo começa com um teste bem escrito.
Mas você pode estar pensando...
“Ué, mas não é só inverter?
Colocareturn totalAmount - discountAmount;
dentro doif
ereturn totalAmount;
fora. Pronto, resolveu!”
Calma... não resolveu. E aqui está o motivo:
O problema não é onde o return
está.
O problema é a lógica da condição.
Veja esse código:
if (discountAmount <= 0 && !valid && expired) {
return totalAmount;
}
return totalAmount - discountAmount;
Agora imagina que o voucher só está expirado, mas é válido e tem desconto positivo.
discountAmount <= 0
→ false!valid
→ falseexpired
→ true
Resultado da condição:false && false && true = false
Ou seja: ele cai no else
e aplica o desconto, mesmo com o voucher expirado!
Esse é o ponto crucial:
Com esse
&&
, o desconto só é bloqueado se todas as três coisas ruins acontecerem ao mesmo tempo. Mas basta UMA coisa estar errada, e já não deveríamos aplicar o desconto.
O que deveria ser feito?
Você precisa inverter a lógica da condição, e não só do return
.
A forma correta é deixar o código desconfiado por padrão, aplicando o desconto somente quando tudo estiver certo:
if (discountAmount > 0 && valid && !expired) {
return totalAmount - discountAmount;
}
return totalAmount;
Agora sim:
O desconto só será aplicado se todas as condições forem verdadeiras, ou seja
✅ O voucher for válido
✅ O voucher não estiver expirado
✅ O valor do desconto for positivo
Se qualquer uma dessas condições falhar, o sistema não aplica o desconto.
Não é questão de "ser mais ou menos válido". O sistema precisa de certeza antes de conceder qualquer benefício.
Essa lógica faz o sistema ser conservador — ele só dá desconto quando tudo está 100% certo. E isso é exatamente o que se espera de uma validação de regras de negócio crítica como essa.
Se qualquer uma falhar — cupom inválido, expirado ou valor inválido — o desconto é negado.
O livro do Vladimir está na minha lista tem um tempo e ler esse artigo só o moveu mais pra cima da lista 😅
Eu tenho um histórico meio peculiar com testes. Comecei minha carreira como front, onde na época o ecossistema de testes era bem menos desenvolvido que hoje (2016/17).
Front, de certa maneira, é mais simples de testar manualmente. Pois estamos ali vendo todas as coisas acontecerem.
Claro que tem mts problemas com isso. Nossos vieses pessoais pra testar apenas o caminho feliz, APIs sempre retornando sucesso, etc.
Conforme fui progredindo na carreira, comecei a ver mais ainda o valor dos testes.
E caí muito nesse ponto que vc comentou: responsabilidade.
Software é algo fantástico. Vc faz uma vez, e virtualmente ele está ao vivo e agora disponível para todo mundo. 24 horas por dia.
E as pessoas hoje em dia esperam que ele esteja sempre disponível e funcionando.
Mas, além disso, as pessoas esperam melhorias e inovações. Mas sem quebrar aquilo que já existe.
Pra mim, hoje em dia escrever um teste é um investimento. Que tem um retorno enorme.
Com essas ferramentas de IA hoje em dia, minha velocidade aumentou muito. As vezes coisas mais simples que demoravam 1 dia são coisas de 1 hora ou até menos.
E isso é sensacional.
Mas isso tbm quer dizer que minha chance pra shippar bugs aumentou ainda mais 😂
E os testes pra mim são como se fosse um funcionário trabalhando 24/7 em todas as minhas mudanças pra impedir eu de quebrar esse delicado balanceamento sobre softwares
Enfim, escrevi uma redação e isso aqui já até estendeu demais.
Mas concordo com tudo que vc escreveu Rafael. Valeu demais por ter tido o tempo de escrever um artigo tão profundo.
Hoje em dia, teste é essencial pra qualquer engenheiro de software responsável. Que se importa não só com o código, mas com que seus usuários nunca sejam prejudicados também
E esse retorno sobre investimento fica ainda maior quando vc lida com grandes equipes de software e produtos complexos.
Pq é muito raro ter uma pessoa que saiba como tudo funciona em todos os domínios diferentes do sistema.
Mas os testes agem não só como segurança, mas como documentação pra cada funcionalidade
Excelente artigo !!
Em momentos que vejo resistência dentro do time, sempre falo que os testes são mais aliados dos dev que da empresa!!
Sugestão.. veja se faz sentido quebrar em 2 o artigo.