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.

Você vai aprender
  • 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 @Test e assertEquals.
  • Ler a saída do mvn test quando tudo passa e quando algo falha.
  • Testar exceções com assertThrows.
  • Preparar os testes com @BeforeEach e 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:

  1. Depende de você olhar com atenção. Um 150.0 que virou 50.0 passa despercebido no meio de 30 linhas.
  2. 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.
  3. 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/java e 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 de Assertions.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 tem main: 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] ------------------------------------------------------------------------
TrechoSignificado
Running br.com.ana.banco.ContaTestO Surefire achou e começou a rodar a classe de testes.
Tests run: 1Um método @Test foi executado.
Failures: 0Nenhuma verificação (assert…) deu errado.
Errors: 0Nenhum teste parou por uma exceção inesperada.
Skipped: 0Nenhum teste foi pulado (é possível desligar um teste com @Disabled).
BUILD SUCCESSTodos 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:

Esta saída mostra um teste falhando de propósito
$ 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 linhas at 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 package nã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 sacar lançou IllegalStateException e o assertEquals nem 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 .class se 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 assertThrows executa quando quiser. Leia como "a ação de sacar 500". Se você escrevesse só conta.sacar(500.0), a exceção estouraria antes de o assertThrows começar.
IllegalStateException erro = …
O assertThrows devolve 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çãoQuando rodaUso típico
@BeforeEachAntes de cada testeCriar os objetos do cenário
@AfterEachDepois de cada testeApagar arquivos temporários
@BeforeAllUma vez, antes de todos (método static)Algo caro, feito uma vez só
@AfterAllUma 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ém ints, strings…). Cada valor vira uma execução, com o valor no parâmetro valor.
@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étodoPassa 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:

  1. 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).
  2. Verde: escreva o mínimo de código para o teste passar.
  3. 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

O teste não roda (e ninguém avisa)

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.

Testes que passam sozinhos e falham juntos

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

ItemPara que serve
@TestMarca um método de teste
@BeforeEach / @AfterEachRoda antes / depois de cada teste
@BeforeAll / @AfterAllRoda 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 / @CsvSourceMesmo teste com vários valores
assertEquals, assertTrue, assertThrows…Verificações (classe Assertions)
mvn testCompila e roda todos os testes
mvn test -Dtest=ContaTestRoda só uma classe de testes
target/surefire-reports/Relatórios de cada classe de teste
./gradlew testO 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:

assertEquals();

3. Esperar uma exceção

Complete para verificar que conta.depositar(-5.0) lança IllegalArgumentException:

(IllegalArgumentException.class, () -> conta.depositar(-5.0));

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-jupiter com <scope>test</scope>; testes em src/test/java, classes terminadas em Test.
  • 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; @BeforeEach prepara cada teste; @DisplayName dá nomes legíveis.
  • @ParameterizedTest com @ValueSource ou @CsvSource roda o mesmo teste com vários valores.
  • TDD: vermelho (teste falha), verde (código mínimo), refatorar.