Java Testes com JUnit
Um teste automatizado é um pequeno programa que usa o seu programa e confere se o resultado está certo. Você escreve uma vez e roda quantas vezes quiser, em segundos. O JUnit é a biblioteca de testes mais usada em Java; nesta aula, a Ana vai testar a sua classe Conta com o JUnit 5 e o Maven.
- Por que testar, e por que "testar à mão" não basta.
- Pôr o JUnit 5 (Jupiter) num projeto Maven.
- Escrever um teste com
@TesteassertEquals. - Ler a saída do
mvn testquando tudo passa e quando algo falha. - Testar exceções com
assertThrows. - Preparar os testes com
@BeforeEache dar nomes legíveis com@DisplayName. - Rodar o mesmo teste com vários valores (testes parametrizados).
- A ideia do TDD.
Você vai precisar de: classes e objetos, exceções e Maven. As anotações (as palavras com @) e as lambdas (() -> …) aparecem aqui só no uso básico.
Por que testar?
Pense numa cozinheira que, a cada mudança na receita do bolo, prova o bolo inteiro de novo: a massa, a cobertura, o recheio. Com uma receita, tudo bem. Com cem receitas, ela passaria o dia provando. Os testes automatizados são um provador que nunca cansa: você diz uma vez "com estes ingredientes, o resultado deve ser este", e ele confere tudo sempre que você pedir.
Até agora, para conferir um programa, você rodava o main e olhava a saída. Isso tem três problemas:
- Depende de você olhar com atenção. Um
150.0que virou50.0passa despercebido no meio de 30 linhas. - Não se repete sozinho. Você muda uma linha hoje e quebra algo que funcionava semana passada (isso tem nome: regressão). Ninguém reconfere tudo à mão a cada mudança.
- Não fica registrado. O que "deveria acontecer" está só na sua cabeça.
Um teste automatizado resolve os três: ele mesmo compara, roda com um comando e documenta o comportamento esperado em código.
A classe que vamos testar: a Conta da Ana
Um projeto Maven (~/banco), com a classe no lugar de sempre, src/main/java:
package br.com.ana.banco;
public class Conta {
private final String titular;
private double saldo;
public Conta(String titular) {
this.titular = titular;
}
public String getTitular() {
return titular;
}
public double getSaldo() {
return saldo;
}
public void depositar(double valor) {
if (valor <= 0) {
throw new IllegalArgumentException("Valor inválido: " + valor);
}
saldo += valor;
}
public void sacar(double valor) {
if (valor <= 0) {
throw new IllegalArgumentException("Valor inválido: " + valor);
}
if (valor > saldo) {
throw new IllegalStateException("Saldo insuficiente");
}
saldo -= valor;
}
}
Regras da conta: começa com saldo zero; depósito e saque precisam ser positivos; não dá para sacar mais do que o saldo. (Num banco de verdade, dinheiro seria BigDecimal; aqui usamos double com valores redondos para manter o foco nos testes.)
1. Pôr o JUnit no projeto
O JUnit é uma biblioteca como outra qualquer: entra no pom.xml como dependência. O pom.xml completo do projeto:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>br.com.ana</groupId>
<artifactId>banco</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.13.4</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
- org.junit.jupiter : junit-jupiter
- O JUnit 5. Jupiter (o planeta Júpiter, porque é o "quinto") é o nome da parte que você usa para escrever testes. Este artefato traz tudo junto: as anotações, as verificações e o "motor" que executa os testes.
- 5.13.4
- A versão. Em 2025 saiu o JUnit 6, com o mesmo Jupiter: o código desta aula funciona igual nele.
- <scope>test</scope>
- "Só para os testes": o JUnit é usado em
src/test/javae não vai para o JAR do programa.
Quem executa os testes no Maven é o plugin Surefire ("à prova de falhas"), que já vem configurado: ele procura as classes de teste em src/test/java e roda todas na fase test.
2. O primeiro teste
Os testes ficam em src/test/java, no mesmo pacote da classe testada, numa classe com o mesmo nome mais Test:
package br.com.ana.banco;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class ContaTest {
@Test
void depositoAumentaOSaldo() {
Conta conta = new Conta("Ana");
conta.depositar(100.0);
assertEquals(100.0, conta.getSaldo());
}
}
- import static …Assertions.assertEquals
- Um import static traz um método estático de outra classe, para usar sem o nome dela:
assertEquals(…)em vez deAssertions.assertEquals(…). Assertion = "afirmação", "verificação". - import org.junit.jupiter.api.Test
- A anotação
@Test. - class ContaTest
- A classe de testes. Não precisa ser
public, e não temmain: quem a executa é o JUnit. - @Test
- Uma anotação: uma etiqueta que diz ao JUnit "este método é um teste, rode-o". Sem ela, o método é ignorado.
- void depositoAumentaOSaldo()
- Um método de teste:
void, sem parâmetros. O nome descreve o que está sendo conferido; ele aparece nos relatórios quando o teste falha. - Conta conta = new Conta("Ana");
- Prepara o cenário.
- conta.depositar(100.0);
- Executa a ação testada.
- assertEquals(100.0, conta.getSaldo());
- Verifica: "o saldo deve ser 100.0". Primeiro o valor esperado, depois o obtido. Se forem diferentes, o teste falha.
Esse formato em três passos (preparar, executar, verificar) é o padrão de quase todo teste. Em inglês é chamado de Arrange, Act, Assert.
Rodar: mvn test
$ mvn test [INFO] Scanning for projects... [INFO] [INFO] --------------------------< br.com.ana:banco >-------------------------- [INFO] Building banco 1.0 [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- … « downloads do JUnit (25 arquivos, só na primeira vez) [INFO] --- compiler:3.16.0:compile (default-compile) @ banco --- [INFO] Recompiling the module because of changed source code. [INFO] Compiling 1 source file with javac [debug release 25] to target/classes [INFO] [INFO] --- resources:3.5.0:testResources (default-testResources) @ banco --- [INFO] skip non existing resourceDirectory /home/ana/banco/src/test/resources [INFO] [INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ banco --- [INFO] Recompiling the module because of changed dependency. [INFO] Compiling 1 source file with javac [debug release 25] to target/test-classes [INFO] [INFO] --- surefire:3.6.0:test (default-test) @ banco --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running br.com.ana.banco.ContaTest [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.063 s -- in br.com.ana.banco.ContaTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 6.460 s [INFO] Finished at: 2026-10-16T18:43:55-03:00 [INFO] ------------------------------------------------------------------------
| Trecho | Significado |
|---|---|
Running br.com.ana.banco.ContaTest | O Surefire achou e começou a rodar a classe de testes. |
Tests run: 1 | Um método @Test foi executado. |
Failures: 0 | Nenhuma verificação (assert…) deu errado. |
Errors: 0 | Nenhum teste parou por uma exceção inesperada. |
Skipped: 0 | Nenhum teste foi pulado (é possível desligar um teste com @Disabled). |
BUILD SUCCESS | Todos passaram. |
3. Quando um teste falha
A Ana acrescentou um segundo teste, com dois depósitos seguidos:
@Test
void doisDepositosSomam() {
Conta conta = new Conta("Ana");
conta.depositar(100.0);
conta.depositar(50.0);
assertEquals(150.0, conta.getSaldo());
}
E, sem perceber, ao mexer na Conta, trocou saldo += valor; por saldo = valor; no depositar. Repare que o primeiro teste continua passando (um depósito de 100 numa conta zerada dá 100 dos dois jeitos). Só o segundo pega o erro:
$ mvn test … [INFO] --- surefire:3.6.0:test (default-test) @ banco --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running br.com.ana.banco.ContaTest [ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.108 s <<< FAILURE! -- in br.com.ana.banco.ContaTest [ERROR] br.com.ana.banco.ContaTest.doisDepositosSomam -- Time elapsed: 0.068 s <<< FAILURE! org.opentest4j.AssertionFailedError: expected: <150.0> but was: <50.0> at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151) at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132) at org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197) at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:70) at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:65) at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:889) at br.com.ana.banco.ContaTest.doisDepositosSomam(ContaTest.java:21) at java.base/java.lang.reflect.Method.invoke(Method.java:565) at java.base/java.util.ArrayList.forEach(ArrayList.java:1604) at java.base/java.util.ArrayList.forEach(ArrayList.java:1604) [INFO] [INFO] Results: [INFO] [ERROR] Failures: [ERROR] ContaTest.doisDepositosSomam:21 expected: <150.0> but was: <50.0> [INFO] [ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [ERROR] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.260 s [INFO] Finished at: 2026-10-16T18:44:09-03:00 [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.6.0:test (default-test) on project banco: There are test failures. [ERROR] [ERROR] See /home/ana/banco/target/surefire-reports for the individual test results. [ERROR] See dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream. [ERROR] -> [Help 1] …
Como ler, por partes:
- Tests run: 2, Failures: 1
- Dois testes rodaram; um falhou.
- ContaTest.doisDepositosSomam … <<< FAILURE!
- Qual teste falhou.
- AssertionFailedError: expected: <150.0> but was: <50.0>
- Por quê: esperava 150.0, mas o saldo era 50.0. Esse é o coração da mensagem.
- at br.com.ana.banco.ContaTest.doisDepositosSomam(ContaTest.java:21)
- Onde: a linha 21 do teste, o
assertEquals. As linhasat org.junit…acima são o caminho dentro do próprio JUnit; pule-as (veja como ler um stack trace em Depurar). - Failures: ContaTest.doisDepositosSomam:21 expected: …
- O resumo no fim: uma linha por falha. Quando há muitas, comece por aqui.
- BUILD FAILURE
- Teste falhando para o build: o
mvn packagenão gera o JAR. É de propósito: código com teste vermelho não deve ser entregue.
O 50.0 é a pista: é o valor do último depósito, ou seja, o saldo foi substituído em vez de somado. A Ana voltou o += e os dois testes passaram. Os mesmos detalhes ficam guardados em target/surefire-reports/br.com.ana.banco.ContaTest.txt.
Missão: o editor do guia não tem o JUnit, mas você pode ver a ideia funcionando "à mão". O método verificar faz o papel do assertEquals, e o main faz o papel do Surefire. Rode e veja a falha; depois conserte a classe Conta (não o teste!) até aparecer 2 de 2 testes passaram.
public class Main {
static int passaram = 0;
static int total = 0;
static void verificar(String nome, double esperado, double obtido) {
total++;
if (esperado == obtido) {
passaram++;
System.out.println("OK " + nome);
} else {
System.out.println("FALHOU " + nome + " ==> expected: <" + esperado + "> but was: <" + obtido + ">");
}
}
public static void main(String[] args) {
Conta c1 = new Conta("Ana");
c1.depositar(100.0);
verificar("depositoAumentaOSaldo", 100.0, c1.getSaldo());
Conta c2 = new Conta("Ana");
c2.depositar(100.0);
c2.depositar(50.0);
verificar("doisDepositosSomam", 150.0, c2.getSaldo());
System.out.println(passaram + " de " + total + " testes passaram");
}
}
class Conta {
private final String titular;
private double saldo;
Conta(String titular) {
this.titular = titular;
}
double getSaldo() {
return saldo;
}
void depositar(double valor) {
saldo = valor;
}
}
public class Main {
static int passaram = 0;
static int total = 0;
static void verificar(String nome, double esperado, double obtido) {
total++;
if (esperado == obtido) {
passaram++;
System.out.println("OK " + nome);
} else {
System.out.println("FALHOU " + nome + " ==> expected: <" + esperado + "> but was: <" + obtido + ">");
}
}
public static void main(String[] args) {
Conta c1 = new Conta("Ana");
c1.depositar(100.0);
verificar("depositoAumentaOSaldo", 100.0, c1.getSaldo());
Conta c2 = new Conta("Ana");
c2.depositar(100.0);
c2.depositar(50.0);
verificar("doisDepositosSomam", 150.0, c2.getSaldo());
System.out.println(passaram + " de " + total + " testes passaram");
}
}
class Conta {
private final String titular;
private double saldo;
Conta(String titular) {
this.titular = titular;
}
double getSaldo() {
return saldo;
}
void depositar(double valor) {
saldo += valor;
}
}
Failure × Error
O Surefire separa dois tipos de problema. Veja um teste que tenta sacar 500 de uma conta vazia, sem esperar exceção nenhuma:
@Test
void saqueSemSaldo() {
Conta conta = new Conta("Ana");
conta.sacar(500.0);
assertEquals(0.0, conta.getSaldo());
}
$ mvn test … [INFO] Running br.com.ana.banco.ContaTest [ERROR] Tests run: 3, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.110 s <<< FAILURE! -- in br.com.ana.banco.ContaTest [ERROR] br.com.ana.banco.ContaTest.saqueSemSaldo -- Time elapsed: 0.013 s <<< ERROR! java.lang.IllegalStateException: Saldo insuficiente at br.com.ana.banco.Conta.sacar(Conta.java:31) at br.com.ana.banco.ContaTest.saqueSemSaldo(ContaTest.java:27) at java.base/java.lang.reflect.Method.invoke(Method.java:565) at java.base/java.util.ArrayList.forEach(ArrayList.java:1604) at java.base/java.util.ArrayList.forEach(ArrayList.java:1604) [INFO] [INFO] Results: [INFO] [ERROR] Errors: [ERROR] ContaTest.saqueSemSaldo:27 » IllegalState Saldo insuficiente [INFO] [ERROR] Tests run: 3, Failures: 0, Errors: 1, Skipped: 0 …
- Failure (falha): uma verificação
assert…deu errado. "O resultado não é o esperado." - Error (erro): o teste parou no meio por uma exceção que ninguém esperava. Aqui, o
sacarlançouIllegalStateExceptione oassertEqualsnem chegou a rodar.
Neste caso, quem está errado é o teste: a conta fez exatamente o que devia (recusar o saque). O certo é testar que a exceção acontece, com assertThrows.
4. Testar exceções: assertThrows
@Test
void saqueSemSaldo() {
Conta conta = new Conta("Ana");
IllegalStateException erro = assertThrows(IllegalStateException.class,
() -> conta.sacar(500.0));
assertEquals("Saldo insuficiente", erro.getMessage());
}
- assertThrows(
- "Verifique que isto lança uma exceção." Importado como o
assertEquals:import static org.junit.jupiter.api.Assertions.assertThrows; - IllegalStateException.class
- Qual exceção é esperada. O
.classse refere à classe da exceção em si (e não a um objeto dela). - () -> conta.sacar(500.0)
- Uma lambda: um "pedaço de código guardado" que o
assertThrowsexecuta quando quiser. Leia como "a ação de sacar 500". Se você escrevesse sóconta.sacar(500.0), a exceção estouraria antes de oassertThrowscomeçar. - IllegalStateException erro = …
- O
assertThrowsdevolve a exceção capturada, para você conferir a mensagem.
Se o código não lançar a exceção, o teste falha com: Expected java.lang.IllegalStateException to be thrown, but nothing was thrown. (você vai ver essa mensagem real na seção de testes parametrizados).
5. Preparar cada teste: @BeforeEach
Todo teste da Conta começa com new Conta("Ana") e, muitas vezes, um depósito inicial. Repetir isso em cada método é cansativo. Um método marcado com @BeforeEach ("antes de cada") roda antes de cada teste, preparando o cenário:
private Conta conta;
@BeforeEach
void criarConta() {
conta = new Conta("Ana");
conta.depositar(100.0);
}
@Test
void saqueDiminuiOSaldo() {
conta.sacar(30.0);
assertEquals(70.0, conta.getSaldo());
}
Importante: o JUnit cria um objeto novo de ContaTest para cada teste, e roda o @BeforeEach de novo. Assim, cada teste começa com uma conta nova de saldo 100, sem sobras do teste anterior. Testes nunca devem depender da ordem em que rodam.
| Anotação | Quando roda | Uso típico |
|---|---|---|
@BeforeEach | Antes de cada teste | Criar os objetos do cenário |
@AfterEach | Depois de cada teste | Apagar arquivos temporários |
@BeforeAll | Uma vez, antes de todos (método static) | Algo caro, feito uma vez só |
@AfterAll | Uma vez, depois de todos (static) | Liberar o que o @BeforeAll criou |
6. Nomes legíveis: @DisplayName
Nomes de método não têm espaço nem acento confortável. @DisplayName ("nome de exibição") dá ao teste ou à classe um nome em português, que aparece nos relatórios e nas IDEs:
@DisplayName("Conta da Ana")
class ContaTest {
@Test
@DisplayName("saque maior que o saldo é recusado")
void saqueSemSaldo() {
…
7. Testes parametrizados: um teste, vários valores
Para conferir que depósitos de 0, -1 e -50 são todos recusados, você escreveria três testes quase iguais. Um teste parametrizado recebe os valores como parâmetro e roda uma vez para cada um:
@ParameterizedTest
@ValueSource(doubles = {0.0, -1.0, -50.0})
@DisplayName("depósito zero ou negativo é recusado")
void depositoInvalido(double valor) {
assertThrows(IllegalArgumentException.class, () -> conta.depositar(valor));
}
@ParameterizedTest
@CsvSource({"10.0, 90.0", "100.0, 0.0", "0.5, 99.5"})
void saquesValidos(double saque, double saldoEsperado) {
conta.sacar(saque);
assertEquals(saldoEsperado, conta.getSaldo());
}
- @ParameterizedTest
- No lugar de
@Test: "teste com parâmetros". - @ValueSource(doubles = {…})
- A fonte dos valores: uma lista de
double(há tambémints,strings…). Cada valor vira uma execução, com o valor no parâmetrovalor. - @CsvSource({"10.0, 90.0", …})
- Para vários parâmetros por execução: cada texto é uma linha "separada por vírgulas" (CSV).
"10.0, 90.0"= sacar 10, saldo esperado 90.
A classe de testes completa
package br.com.ana.banco;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
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;
@DisplayName("Conta da Ana")
class ContaTest {
private Conta conta;
@BeforeEach
void criarConta() {
conta = new Conta("Ana");
conta.depositar(100.0);
}
@Test
@DisplayName("depósito aumenta o saldo")
void depositoAumentaOSaldo() {
conta.depositar(50.0);
assertEquals(150.0, conta.getSaldo());
}
@Test
@DisplayName("saque diminui o saldo")
void saqueDiminuiOSaldo() {
conta.sacar(30.0);
assertEquals(70.0, conta.getSaldo());
}
@Test
@DisplayName("saque maior que o saldo é recusado")
void saqueSemSaldo() {
IllegalStateException erro = assertThrows(IllegalStateException.class,
() -> conta.sacar(500.0));
assertEquals("Saldo insuficiente", erro.getMessage());
assertEquals(100.0, conta.getSaldo());
}
@ParameterizedTest
@ValueSource(doubles = {0.0, -1.0, -50.0})
@DisplayName("depósito zero ou negativo é recusado")
void depositoInvalido(double valor) {
assertThrows(IllegalArgumentException.class, () -> conta.depositar(valor));
}
@ParameterizedTest
@CsvSource({"10.0, 90.0", "100.0, 0.0", "0.5, 99.5"})
void saquesValidos(double saque, double saldoEsperado) {
conta.sacar(saque);
assertEquals(saldoEsperado, conta.getSaldo());
}
}
$ mvn test … [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running Conta da Ana [INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.212 s -- in Conta da Ana [INFO] [INFO] Results: [INFO] [INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS
9 testes: os 3 com @Test, mais 3 execuções de depositoInvalido e 3 de saquesValidos. E a classe aparece como Conta da Ana, o nome do @DisplayName.
Os testes protegendo a conta
Para ver a rede de segurança em ação, a Ana apagou de propósito a verificação if (valor <= 0) do depositar. Os testes parametrizados denunciaram na hora (stack traces encurtados):
$ mvn test … [INFO] Running Conta da Ana [ERROR] Tests run: 9, Failures: 3, Errors: 0, Skipped: 0, Time elapsed: 0.236 s <<< FAILURE! -- in Conta da Ana [ERROR] br.com.ana.banco.ContaTest.depositoInvalido(double)[1] -- Time elapsed: 0.013 s <<< FAILURE! org.opentest4j.AssertionFailedError: Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown. at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152) at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:73) at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35) at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3128) at br.com.ana.banco.ContaTest.depositoInvalido(ContaTest.java:51) at java.base/java.lang.reflect.Method.invoke(Method.java:565) … « mais 34 linhas "at java.base/…" [ERROR] br.com.ana.banco.ContaTest.depositoInvalido(double)[2] -- Time elapsed: 0.003 s <<< FAILURE! … [ERROR] br.com.ana.banco.ContaTest.depositoInvalido(double)[3] -- Time elapsed: 0.005 s <<< FAILURE! … [INFO] Results: [INFO] [ERROR] Failures: [ERROR] ContaTest.depositoInvalido:51 Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown. [ERROR] ContaTest.depositoInvalido:51 Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown. [ERROR] ContaTest.depositoInvalido:51 Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown. [INFO] [ERROR] Tests run: 9, Failures: 3, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [ERROR] BUILD FAILURE
[1], [2] e [3] são as três execuções (com 0.0, -1.0 e -50.0). Nenhuma lançou a exceção esperada. Sem os testes, um depósito negativo passaria a "sacar" dinheiro da conta, e ninguém perceberia até um cliente reclamar.
8. As verificações mais usadas
| Método | Passa quando… |
|---|---|
assertEquals(esperado, obtido) | os dois são iguais (com equals para objetos) |
assertEquals(0.3, x, 0.0001) | o double x está a no máximo 0.0001 de 0.3 (para contas com casas decimais, que raramente dão exato; veja double e float) |
assertNotEquals(a, b) | são diferentes |
assertTrue(condição) / assertFalse(condição) | a condição é true / false |
assertNull(x) / assertNotNull(x) | x é / não é null |
assertThrows(Tipo.class, () -> …) | o código lança aquela exceção |
assertDoesNotThrow(() -> …) | o código não lança nada |
assertAll(() -> …, () -> …) | todas as verificações passam (e mostra todas as que falharam, não só a primeira) |
fail("mensagem") | nunca: falha na hora |
Todas aceitam uma mensagem extra no fim, mostrada quando falham: assertEquals(150.0, saldo, "saldo depois de dois depósitos").
9. A ideia do TDD
TDD (Test-Driven Development, "desenvolvimento guiado por testes") é um jeito de trabalhar em que o teste vem antes do código. É um ciclo curto, de três passos, com nomes de cores:
- Vermelho: escreva um teste para algo que ainda não existe (por exemplo, "transferir 30 da conta da Ana para a do Bruno"). Rode: ele falha (nem compila, ou dá
FAILURE). - Verde: escreva o mínimo de código para o teste passar.
- Refatorar: com o teste verde protegendo, melhore o código (nomes, repetições) sem mudar o comportamento. Rode de novo.
A vantagem: você pensa primeiro em como a classe vai ser usada, e termina com todo o código coberto por testes. Não é obrigatório; muita gente escreve o teste logo depois do código. O importante é que o teste exista.
Missão (no estilo TDD): o "teste" no main já está escrito e espera que sacar 500 de uma conta com 100 lance IllegalStateException com a mensagem Saldo insuficiente, sem mexer no saldo. Rode: ele falha (vermelho). Agora escreva o método sacar na Conta até aparecer OK: Saldo insuficiente e Saldo: 100.0 (verde).
public class Main {
public static void main(String[] args) {
Conta conta = new Conta("Ana");
conta.depositar(100.0);
try {
conta.sacar(500.0);
System.out.println("FALHOU: Expected IllegalStateException to be thrown, but nothing was thrown.");
} catch (IllegalStateException e) {
System.out.println("OK: " + e.getMessage());
}
System.out.println("Saldo: " + conta.getSaldo());
}
}
class Conta {
private final String titular;
private double saldo;
Conta(String titular) {
this.titular = titular;
}
double getSaldo() {
return saldo;
}
void depositar(double valor) {
saldo += valor;
}
void sacar(double valor) {
saldo -= valor;
}
}
public class Main {
public static void main(String[] args) {
Conta conta = new Conta("Ana");
conta.depositar(100.0);
try {
conta.sacar(500.0);
System.out.println("FALHOU: Expected IllegalStateException to be thrown, but nothing was thrown.");
} catch (IllegalStateException e) {
System.out.println("OK: " + e.getMessage());
}
System.out.println("Saldo: " + conta.getSaldo());
}
}
class Conta {
private final String titular;
private double saldo;
Conta(String titular) {
this.titular = titular;
}
double getSaldo() {
return saldo;
}
void depositar(double valor) {
saldo += valor;
}
void sacar(double valor) {
if (valor > saldo) {
throw new IllegalStateException("Saldo insuficiente");
}
saldo -= valor;
}
}
Quando dá errado
A Ana criou uma classe ContaTeste (em português, com e no fim) com um teste que deveria falhar. O resultado:
$ mvn test … [INFO] Compiling 2 source files with javac [debug release 25] to target/test-classes … [INFO] Running Conta da Ana [INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.229 s -- in Conta da Ana … [INFO] BUILD SUCCESS
O que aconteceu: a classe foi compilada (2 source files), mas o Surefire só roda classes cujo nome termina em Test, Tests ou TestCase, ou começa com Test. ContaTeste foi ignorada em silêncio. Como resolver: renomeie para ContaTest (ou ContaTests). Outra causa do mesmo sintoma: esquecer o @Test no método.
package org.junit.jupiter.api does not exist
$ mvn clean test … [ERROR] COMPILATION ERROR : [INFO] ------------------------------------------------------------- [ERROR] /home/ana/banco/src/test/java/br/com/ana/banco/ContaTest.java:[3,36] package org.junit.jupiter.api does not exist [ERROR] /home/ana/banco/src/test/java/br/com/ana/banco/ContaTest.java:[3,1] static import only from classes and interfaces [ERROR] /home/ana/banco/src/test/java/br/com/ana/banco/ContaTest.java:[4,36] package org.junit.jupiter.api does not exist … « mais 27 erros [INFO] 30 errors … [ERROR] BUILD FAILURE
O que aconteceu: o JUnit não está no pom.xml (aqui, a Ana apagou o bloco <dependencies> para ver). Um esquecimento vira 30 erros, porque cada uso do JUnit falha. Como resolver: leia só o primeiro, e acrescente a dependência junit-jupiter.
expected: <0.3> but was: <0.30000000000000004>
Um teste com centavos (rodado sozinho com -Dtest=, que escolhe a classe de testes):
@Test
void depositosDeCentavos() {
Conta conta = new Conta("Ana");
conta.depositar(0.1);
conta.depositar(0.2);
assertEquals(0.3, conta.getSaldo());
}
$ mvn test -Dtest=CentavosTest … [INFO] Running br.com.ana.banco.CentavosTest [ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.081 s <<< FAILURE! -- in br.com.ana.banco.CentavosTest [ERROR] br.com.ana.banco.CentavosTest.depositosDeCentavos -- Time elapsed: 0.051 s <<< FAILURE! org.opentest4j.AssertionFailedError: expected: <0.3> but was: <0.30000000000000004> … [ERROR] Failures: [ERROR] CentavosTest.depositosDeCentavos:14 expected: <0.3> but was: <0.30000000000000004> …
O que aconteceu: a Conta não tem defeito: contas com double quase nunca dão o valor exato (0.1 + 0.2 não é 0.3; veja double e float). Como resolver: use a versão com tolerância, assertEquals(0.3, conta.getSaldo(), 0.0001), ou, para dinheiro de verdade, BigDecimal.
O que aconteceu: os testes estão dividindo algo (um atributo static, um arquivo) e um deixa sujeira para o outro. Como resolver: crie tudo de novo no @BeforeEach; não use static para guardar estado entre testes.
Referência
| Item | Para que serve |
|---|---|
@Test | Marca um método de teste |
@BeforeEach / @AfterEach | Roda antes / depois de cada teste |
@BeforeAll / @AfterAll | Roda uma vez antes / depois de todos (static) |
@DisplayName("…") | Nome legível do teste ou da classe |
@Disabled("motivo") | Desliga um teste (conta como Skipped) |
@ParameterizedTest + @ValueSource / @CsvSource | Mesmo teste com vários valores |
assertEquals, assertTrue, assertThrows… | Verificações (classe Assertions) |
mvn test | Compila e roda todos os testes |
mvn test -Dtest=ContaTest | Roda só uma classe de testes |
target/surefire-reports/ | Relatórios de cada classe de teste |
./gradlew test | O mesmo no Gradle |
Exercícios
1. A anotação
Complete para o JUnit reconhecer o método como teste:
void saqueDiminuiOSaldo() { … }
2. A ordem dos argumentos
O saldo esperado é 70.0. Complete a verificação:
3. Esperar uma exceção
Complete para verificar que conta.depositar(-5.0) lança IllegalArgumentException:
4. O resumo diz Tests run: 5, Failures: 0, Errors: 1. O que aconteceu?
5. Por que o primeiro teste (depositoAumentaOSaldo) não pegou o erro saldo = valor?
6. Quantas vezes roda um @ParameterizedTest com @CsvSource({"1, 2", "3, 4", "5, 6", "7, 8"})?
Resumo
- Testes automatizados conferem o programa sozinhos, em segundos, a cada mudança, e pegam regressões.
- No Maven: dependência
org.junit.jupiter:junit-jupitercom<scope>test</scope>; testes emsrc/test/java, classes terminadas emTest. - Um teste:
@Test+ preparar, executar, verificar.assertEquals(esperado, obtido). mvn test:Tests run / Failures / Errors / Skipped. Failure = verificação errada; Error = exceção inesperada. Teste falhando =BUILD FAILURE.assertThrows(Tipo.class, () -> …)testa exceções;@BeforeEachprepara cada teste;@DisplayNamedá nomes legíveis.@ParameterizedTestcom@ValueSourceou@CsvSourceroda o mesmo teste com vários valores.- TDD: vermelho (teste falha), verde (código mínimo), refatorar.