Uma suíte pode executar o código inteiro e ainda deixar passar um defeito óbvio. Isso acontece quando o teste só chama métodos, mas não verifica um resultado relevante. Vamos construir testes que explicam uma regra e conseguem detectar sua violação.
Você precisa conhecer classes, condições, exceções e o build com Maven. Usaremos Java 21 e JUnit Jupiter 5.11.4 em um projeto Java puro. Não precisamos iniciar Spring nem um banco para verificar uma regra aritmética. Ao final, você vai conseguir apontar qual defeito cada teste detecta e distinguir o que ele ainda não prova.
O que é um teste automatizado?
É um programa que prepara uma situação, executa uma ação e compara o comportamento com uma expectativa. Uma assertion é essa verificação executável. Se a expectativa não se confirma, o teste falha.
A expectativa precisa vir do requisito. Copiar o mesmo cálculo da implementação para dentro do teste pode reproduzir o mesmo erro nos dois lugares. Prefira casos pequenos cujo resultado você consegue justificar manualmente.
Vamos usar um estacionamento fictício. A regra é: a primeira hora custa 12 reais; cada hora adicional custa 8; só aceitamos horas inteiras entre 1 e 24. O valor será calculado em centavos, evitando ponto flutuante.
Antes do código, os resultados são: 1 hora = 1200, 2 horas = 2000, 3 horas = 2800, 24 horas = 19600. Zero e 25 horas são entradas inválidas. Esses exemplos ajudam a revelar se entendemos a regra.
Uma implementação pequena e observável
Crie src/main/java/Tarifa.java:
public final class Tarifa {
public int calcularCentavos(int horas) {
if (horas < 1 || horas > 24) {
throw new IllegalArgumentException("Horas devem estar entre 1 e 24");
}
return 1200 + (horas - 1) * 800;
}
}
Subtraímos um porque a primeira hora já foi cobrada. O limite superior também mantém o cálculo em uma faixa segura para int neste domínio. Um valor fixo em centavos funciona aqui porque estamos somando quantias inteiras dessa unidade; não é uma solução universal para câmbio, juros ou rateios.
Como o Maven encontra o JUnit
No POM do projeto de ambiente, mantenha a configuração do compilador e acrescente a dependência a seguir dentro de <dependencies>:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
Dentro de <build><plugins>, junto do plugin do compilador, acrescente:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
A dependência fornece APIs e suporte ao JUnit Jupiter; o Surefire executa testes na fase test. São versões fixas para este exemplo, sem promessa de serem as mais recentes. Em um projeto Spring Boot, as versões podem ser gerenciadas pelo parent/BOM do projeto; não copie versões manualmente sem verificar essa gestão.
O teste mostra o contrato
Crie src/test/java/TarifaTest.java:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class TarifaTest {
@Test
void primeiraHoraCustaDozeReais() {
Tarifa tarifa = new Tarifa();
int resultado = tarifa.calcularCentavos(1);
assertEquals(1200, resultado);
}
@ParameterizedTest
@CsvSource({"2,2000", "3,2800", "24,19600"})
void cobraOitoReaisPorHoraAdicional(int horas, int esperado) {
assertEquals(esperado, new Tarifa().calcularCentavos(horas));
}
@ParameterizedTest
@ValueSource(ints = {-1, 0, 25})
void rejeitaDuracaoForaDoIntervalo(int horas) {
assertThrows(IllegalArgumentException.class,
() -> new Tarifa().calcularCentavos(horas));
}
}
Execute mvn test. A expectativa é sete execuções bem-sucedidas: uma simples, três linhas da tabela e três entradas inválidas.
O primeiro teste separa preparação, ação e verificação. O teste parametrizado aplica a mesma ideia a dados diferentes, sem repetir o corpo. assertThrows recebe uma função: ele executa a ação e verifica que a exceção esperada ocorre. A lambda adia a chamada para dentro da assertion.
Se o teste falhar, leia primeiro o resultado esperado e o observado. Depois volte ao requisito. Nem toda falha significa que a implementação está errada: o teste também pode conter uma expectativa incorreta ou uma preparação que não corresponde ao caso descrito.
Prove que o teste detecta alguma coisa
Troque temporariamente (horas - 1) por horas. O caso de uma hora passa a retornar 2000; o teste deve falhar. Depois restaure o cálculo e troque horas < 1 por horas < 0. Agora zero é aceito e o teste de entrada inválida deve acusar.
Isso é uma experiência de mutação: introduzir um defeito controlado para observar se a suíte percebe. Ferramentas podem automatizar mutações, mas a pergunta vem antes delas. Se nenhum teste falha, há uma lacuna ou a mudança não altera comportamento relevante para os casos existentes.
Cobertura de linhas informa quais linhas foram executadas. Não prova a força das verificações. Um teste sem assertion pode executar uma linha de cálculo e não detectar que seu resultado está errado.
Estado também precisa de teste
Nem toda operação devolve um número. No estoque estudado anteriormente, uma reserva inválida precisa deixar a quantidade intacta. O teste deve preparar estoque de três, tentar reservar cinco, verificar a exceção e consultar novamente o disponível. A última verificação prova a ausência de alteração parcial.
Verificar somente a exceção deixaria passar uma implementação que subtrai primeiro e reclama depois. O nome do teste pode expressar isso: reservaRecusadaPreservaDisponivel. Nomes são documentação útil quando descrevem a decisão e não apenas teste1.
Quando um substituto ajuda — e quando engana
Um test double substitui um colaborador durante o teste. Um fake implementa uma versão simplificada, como um repositório em memória. Um mock permite configurar respostas e verificar interações. Mockito é uma ferramenta para criar esses mocks, mas não é necessário mockar a classe que estamos tentando testar.
Se um serviço chama um provedor de e-mail, um substituto evita envio real e permite verificar a mensagem solicitada. Esse teste não prova que a integração HTTP com o provedor funciona. Para isso, é necessário outro nível de teste com o contrato real ou um ambiente adequado.
Use teste unitário para uma unidade de comportamento com dependências controladas. Use teste de integração quando precisa observar componentes trabalhando juntos, como aplicação e banco. Uma unidade não precisa ser sinônimo de um único método privado; teste a interface que expressa a regra.
Pratique com uma regra nova
Agora existe um teto de 10 mil centavos por permanência. Modifique primeiro os exemplos esperados: 1 hora continua 1200; 12 horas custariam 10000; 13 e 24 também devem custar 10000. Depois implemente o teto.
Uma resposta possível calcula o preço normalmente e devolve Math.min(preco, 10000), mantendo a validação de horas. O teste de 25 horas deve continuar esperando exceção: teto de cobrança não elimina limite de duração. Essa distinção evita corrigir uma regra quebrando outra.
Em outra sessão, explique qual teste detecta cobrança de uma hora extra e qual detecta aceitar zero. Se você consegue ligar requisito, caso e defeito, está pronto para levar essa base à API e acrescentar testes de integração sem abandonar a regra isolada.
Referência usada: guia de regras Java para exercitar com testes.
Pratique com /aprender
Copie o comando e cole no chat para iniciar uma prática guiada:
/aprender Pratique testes unitários de uma regra Java com JUnit 5. Apresente uma função de desconto e peça casos que fariam o teste falhar se a regra estivesse errada. Não inicie um projeto nem mostre a solução antes da minha tentativa; peça justificativa para cada caso.
