Projeto Conta Bancária com Testes
Dinheiro não aceita "quase certo". Neste projeto você constrói uma conta bancária que deposita, saca e transfere com centavos exatos, recusa saques sem saldo com uma exceção própria e, o mais importante, vem com testes automáticos: pequenos programas que conferem a conta inteira com um único comando, mvn test. No fim, você vai quebrar o código de propósito e ver os testes pegarem o erro antes de qualquer cliente.
- Montar um projeto Maven do zero, com o JUnit como dependência.
- Representar dinheiro com BigDecimal (e entender por que não
double). - Encapsulamento: o saldo só muda por depósito, saque e transferência.
- Exceção própria checked:
SaldoInsuficienteException. - Testes com JUnit 5:
@Test,@BeforeEach,assertEquals,assertThrows. - Ler um relatório de teste que falhou e consertar o código.
O que vamos construir
Um projeto Maven com quatro arquivos de código e um pom.xml:
$ find . -type f | sort ./pom.xml ./src/main/java/br/com/ana/banco/App.java ./src/main/java/br/com/ana/banco/Conta.java ./src/main/java/br/com/ana/banco/SaldoInsuficienteException.java ./src/test/java/br/com/ana/banco/ContaTest.java
Sete testes que rodam em menos de um segundo e um programa de demonstração:
$ mvn test … « compilação: veja a saída completa na etapa 3 [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running br.com.ana.banco.ContaTest [INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.100 s -- in br.com.ana.banco.ContaTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS … $ java -cp target/classes br.com.ana.banco.App Ana: R$ 749.25 Bruno: R$ 250.75 Erro: Saldo insuficiente: saldo 250.75, pedido 300.00
- O projeto Maven (pastas e
pom.xml). - A conta e o dinheiro exato (
BigDecimal, depósito). - O primeiro teste (JUnit,
mvn test). - Saque e SaldoInsuficienteException (
assertThrows). - Transferência (um método que usa os outros dois).
- Uma falha de propósito (o teste vermelho e a correção).
Antes de começar
Leia antes Maven e Testes com JUnit: lá cada linha do pom.xml e cada anotação do JUnit é explicada com calma. Aqui vamos usar tudo junto. Na aula de JUnit a conta guardava o saldo num double; neste projeto vamos fazer do jeito certo para dinheiro.
O editor do guia não roda Maven nem JUnit. Por isso, as caixas Experimente usam a mesma Conta num arquivo só, com um main que confere os resultados "na mão". As saídas de mvn desta página foram rodadas de verdade, na máquina da Ana; repita-as no seu computador.
Etapa 1 · O projeto Maven
O Maven espera uma estrutura de pastas fixa: código em src/main/java, testes em src/test/java, e dentro delas as pastas do pacote. O nosso pacote é br.com.ana.banco (o "endereço" das classes, ao contrário do domínio de um site), que vira as pastas br/com/ana/banco:
$ mkdir -p java/banco $ cd java/banco $ mkdir -p src/main/java/br/com/ana/banco src/test/java/br/com/ana/banco
Na raiz do projeto, o pom.xml, igual ao da aula de JUnit:
<?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>
| Trecho | Para quê |
|---|---|
groupId, artifactId, version | A identidade do projeto: br.com.ana:banco:1.0. |
maven.compiler.release = 25 | Compila para o Java 25. |
sourceEncoding = UTF-8 | Acentos nos textos ("Saldo insuficiente…") saem certos. |
dependência junit-jupiter com <scope>test</scope> | O JUnit 5. O escopo test diz que ele só é usado para compilar e rodar os testes, não vai junto com o programa. |
Para conferir que o projeto está montado, uma classe mínima, App, com um main:
package br.com.ana.banco;
public class App {
public static void main(String[] args) {
System.out.println("Banco da Ana: projeto funcionando!");
}
}
$ mvn compile [INFO] Scanning for projects... [INFO] [INFO] --------------------------< br.com.ana:banco >-------------------------- [INFO] Building banco 1.0 [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] Loaded 24119 auto-discovered prefixes for remote repository central (prefixes-central.txt) [INFO] [INFO] --- resources:3.5.0:resources (default-resources) @ banco --- [INFO] skip non existing resourceDirectory /home/ana/java/banco/src/main/resources [INFO] [INFO] --- compiler:3.16.0:compile (default-compile) @ banco --- [INFO] Loaded 74 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt) [INFO] Recompiling the module because of changed source code. [INFO] Compiling 1 source file with javac [debug release 25] to target/classes [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.556 s [INFO] Finished at: 2026-10-16T18:53:15-03:00 [INFO] ------------------------------------------------------------------------ $ java -cp target/classes br.com.ana.banco.App Banco da Ana: projeto funcionando!
- mvn compile
- Compila o código de
src/main/javae põe os.classemtarget/classes. Na primeira vez, antes dessas linhas aparecem dezenas deDownloading from central…: o Maven baixando os plugins que usa. Depois fica tudo guardado em~/.m2. - java -cp target/classes
-cp(classpath, "caminho das classes") diz onde procurar os.class.- br.com.ana.banco.App
- O nome completo da classe, com o pacote na frente.
Etapa 2 · A conta e o dinheiro exato
Por que não guardar o saldo num double? Porque o double guarda números em binário, e a maioria dos centavos não tem representação exata em binário (como 1/3 não tem em decimal). Veja no JShell:
$ jshell
| Welcome to JShell -- Version 25.0.4.1
| For an introduction type: /help intro
jshell> 0.1 + 0.2
$1 ==> 0.30000000000000004
jshell> new java.math.BigDecimal("0.1").add(new java.math.BigDecimal("0.2"))
$2 ==> 0.3
Para um jogo, 0.30000000000000004 não faz diferença. Para um banco, depois de milhões de operações, faz. O BigDecimal guarda o número em decimal, dígito por dígito, e as contas saem exatas. A Conta, por enquanto só com depósito:
package br.com.ana.banco;
import java.math.BigDecimal;
public class Conta {
private final String titular;
private BigDecimal saldo = BigDecimal.ZERO;
public Conta(String titular) {
this.titular = titular;
}
public String getTitular() {
return titular;
}
public BigDecimal getSaldo() {
return saldo;
}
public void depositar(BigDecimal valor) {
validar(valor);
saldo = saldo.add(valor);
}
private static void validar(BigDecimal valor) {
if (valor == null || valor.signum() <= 0) {
throw new IllegalArgumentException("O valor deve ser positivo: " + valor);
}
}
}
| Trecho | O que faz |
|---|---|
package br.com.ana.banco; | Primeira linha de todo arquivo do projeto: o pacote, igual às pastas. |
BigDecimal.ZERO | Uma constante pronta com o valor 0. |
saldo = saldo.add(valor); | BigDecimal não usa +: usa métodos (add, subtract, multiply). E ele é imutável: add devolve um número novo, que precisa ser guardado de volta em saldo. Só escrever saldo.add(valor); não muda nada. |
valor.signum() <= 0 | signum ("sinal") devolve -1, 0 ou 1. Recusamos zero e negativos. |
private static void validar | Um método auxiliar só da classe; o depósito e (logo mais) o saque usam o mesmo. |
Sem setSaldo | O saldo só muda por operações bancárias (encapsulamento). |
Um App para experimentar, que também mostra a conta com double e um depósito inválido:
package br.com.ana.banco;
import java.math.BigDecimal;
public class App {
public static void main(String[] args) {
Conta ana = new Conta("Ana");
ana.depositar(new BigDecimal("0.10"));
ana.depositar(new BigDecimal("0.20"));
System.out.println(ana.getTitular() + ": R$ " + ana.getSaldo());
double d = 0.10 + 0.20;
System.out.println("Com double: " + d);
ana.depositar(new BigDecimal("-5.00"));
}
}
$ mvn -q compile $ java -cp target/classes br.com.ana.banco.App Ana: R$ 0.30 Com double: 0.30000000000000004 Exception in thread "main" java.lang.IllegalArgumentException: O valor deve ser positivo: -5.00 at br.com.ana.banco.Conta.validar(Conta.java:28) at br.com.ana.banco.Conta.depositar(Conta.java:22) at br.com.ana.banco.App.main(App.java:15)
O -q (quiet, "quieto") faz o Maven só falar se der erro. Repare no 0.30, com duas casas: o BigDecimal lembra a escala (o número de casas decimais) dos valores que você usou. Isso vai importar na etapa 3.
BigDecimal a partir de texto
new BigDecimal("0.1") é exatamente 0,1. Já new BigDecimal(0.1) recebe o double, que já nasceu errado, e mostra o erro todo: 0.1000000000000000055511151231257827021181583404541015625. Use sempre a versão com aspas (ou BigDecimal.valueOf(0.1)). Detalhes em BigDecimal e Dinheiro.
Missão (etapa 2): este programa soma centavos com double e mostra 0.30000000000000004. Reescreva com BigDecimal (comece em BigDecimal.ZERO e use add com new BigDecimal("0.10")) para mostrar Saldo: 0.30.
public class Main {
public static void main(String[] args) {
double saldo = 0;
saldo = saldo + 0.10;
saldo = saldo + 0.20;
System.out.println("Saldo: " + saldo);
}
}
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
BigDecimal saldo = BigDecimal.ZERO;
saldo = saldo.add(new BigDecimal("0.10"));
saldo = saldo.add(new BigDecimal("0.20"));
System.out.println("Saldo: " + saldo);
}
}
Etapa 3 · O primeiro teste
Até agora, para saber se a conta funciona, você roda o App e olha a saída. Um teste automático faz isso por você: ele chama a conta e confere o resultado, sem ninguém precisar olhar. O arquivo fica em src/test/java, no mesmo pacote:
package br.com.ana.banco;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.math.BigDecimal;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ContaTest {
private Conta ana;
private Conta bruno;
@BeforeEach
void preparar() {
ana = new Conta("Ana");
bruno = new Conta("Bruno");
}
@Test
void depositoAumentaOSaldo() {
ana.depositar(new BigDecimal("100.50"));
assertEquals(new BigDecimal("100.50"), ana.getSaldo());
}
@Test
void centavosSomamSemErro() {
ana.depositar(new BigDecimal("0.10"));
ana.depositar(new BigDecimal("0.20"));
assertEquals(new BigDecimal("0.30"), ana.getSaldo());
}
@Test
void depositoNegativoEhRecusado() {
assertThrows(IllegalArgumentException.class, () -> ana.depositar(new BigDecimal("-5.00")));
}
}
- import static …Assertions.assertEquals;
- Um import static traz o método para usar sem o nome da classe:
assertEquals(…)em vez deAssertions.assertEquals(…). - @BeforeEach void preparar()
- Roda antes de cada teste: cada um começa com contas novas, zeradas, sem depender dos outros.
- @Test
- Marca um método como teste. O nome do método descreve o que se espera:
depositoAumentaOSaldo. - assertEquals(esperado, obtido)
- "Afirme que são iguais." Se não forem, o teste falha. Primeiro o esperado, depois o que o código produziu.
- assertThrows(IllegalArgumentException.class, () -> …)
- "Afirme que isto lança essa exceção." O
() -> …é uma lambda: um pedaço de código que o JUnit roda dentro de uma proteção. Se a exceção não vier, o teste falha.
$ mvn test [INFO] Scanning for projects... [INFO] [INFO] --------------------------< br.com.ana:banco >-------------------------- [INFO] Building banco 1.0 [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] Loaded 24119 auto-discovered prefixes for remote repository central (prefixes-central.txt) [INFO] [INFO] --- resources:3.5.0:resources (default-resources) @ banco --- [INFO] skip non existing resourceDirectory /home/ana/java/banco/src/main/resources [INFO] [INFO] --- compiler:3.16.0:compile (default-compile) @ banco --- [INFO] Loaded 74 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt) [INFO] Recompiling the module because of changed source code. [INFO] Compiling 2 source files 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/java/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: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.098 s -- in br.com.ana.banco.ContaTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.864 s [INFO] Finished at: 2026-10-16T18:53:29-03:00 [INFO] ------------------------------------------------------------------------
O mvn test compila o código (compile), compila os testes (testCompile, em target/test-classes) e roda os testes com o plugin surefire. Tests run: 3, Failures: 0: três testes, nenhum falhou. BUILD SUCCESS é a luz verde.
A Ana escreveu o primeiro teste assim:
ana.depositar(new BigDecimal("100.00"));
assertEquals(new BigDecimal("100"), ana.getSaldo());
$ mvn test … « compilação igual à anterior [INFO] Running br.com.ana.banco.ContaTest [ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.120 s <<< FAILURE! -- in br.com.ana.banco.ContaTest [ERROR] br.com.ana.banco.ContaTest.depositoAumentaOSaldo -- Time elapsed: 0.010 s <<< FAILURE! org.opentest4j.AssertionFailedError: expected: <100> but was: <100.00> 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:182) at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177) at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1145) at br.com.ana.banco.ContaTest.depositoAumentaOSaldo(ContaTest.java:23) 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.depositoAumentaOSaldo:23 expected: <100> but was: <100.00> [INFO] [ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [ERROR] BUILD FAILURE … « mais algumas linhas [ERROR] com dicas do Maven
"Esperado 100, mas era 100.00." Para o equals do BigDecimal, 100 (escala 0) e 100.00 (escala 2) são diferentes, e o assertEquals usa o equals. O código da conta está certo; o teste é que comparou do jeito errado. Duas saídas: escrever o esperado com a mesma escala (new BigDecimal("100.00"), como fizemos) ou comparar com compareTo, que ignora a escala: assertEquals(0, new BigDecimal("100").compareTo(ana.getSaldo())).
Missão (etapa 3): sem JUnit, o método confere faz o papel do assertEquals. Escreva o segundo teste: uma conta nova do Bruno, depósitos de 0.10 e 0.20, e confere("centavos somam sem erro", …) com o esperado 0.30.
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
Conta ana = new Conta("Ana");
ana.depositar(new BigDecimal("100.50"));
confere("depósito aumenta o saldo", new BigDecimal("100.50"), ana.getSaldo());
// segundo teste: uma conta nova, depósitos de 0.10 e 0.20, saldo esperado 0.30
}
static void confere(String teste, BigDecimal esperado, BigDecimal obtido) {
if (esperado.equals(obtido)) {
System.out.println("OK: " + teste);
} else {
System.out.println("FALHOU: " + teste + " (esperado " + esperado + ", obtido " + obtido + ")");
}
}
}
class Conta {
private final String titular;
private BigDecimal saldo = BigDecimal.ZERO;
Conta(String titular) {
this.titular = titular;
}
String getTitular() {
return titular;
}
BigDecimal getSaldo() {
return saldo;
}
void depositar(BigDecimal valor) {
validar(valor);
saldo = saldo.add(valor);
}
private static void validar(BigDecimal valor) {
if (valor == null || valor.signum() <= 0) {
throw new IllegalArgumentException("O valor deve ser positivo: " + valor);
}
}
}
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
Conta ana = new Conta("Ana");
ana.depositar(new BigDecimal("100.50"));
confere("depósito aumenta o saldo", new BigDecimal("100.50"), ana.getSaldo());
Conta bruno = new Conta("Bruno");
bruno.depositar(new BigDecimal("0.10"));
bruno.depositar(new BigDecimal("0.20"));
confere("centavos somam sem erro", new BigDecimal("0.30"), bruno.getSaldo());
}
static void confere(String teste, BigDecimal esperado, BigDecimal obtido) {
if (esperado.equals(obtido)) {
System.out.println("OK: " + teste);
} else {
System.out.println("FALHOU: " + teste + " (esperado " + esperado + ", obtido " + obtido + ")");
}
}
}
class Conta {
private final String titular;
private BigDecimal saldo = BigDecimal.ZERO;
Conta(String titular) {
this.titular = titular;
}
String getTitular() {
return titular;
}
BigDecimal getSaldo() {
return saldo;
}
void depositar(BigDecimal valor) {
validar(valor);
saldo = saldo.add(valor);
}
private static void validar(BigDecimal valor) {
if (valor == null || valor.signum() <= 0) {
throw new IllegalArgumentException("O valor deve ser positivo: " + valor);
}
}
}
Etapa 4 · Saque e SaldoInsuficienteException
Sacar mais do que se tem não é um "erro de programação": é uma situação normal do banco, que quem chama o sacar precisa tratar (mostrar uma mensagem, oferecer um valor menor…). Por isso a exceção é checked (estende Exception):
package br.com.ana.banco;
import java.math.BigDecimal;
public class SaldoInsuficienteException extends Exception {
public SaldoInsuficienteException(BigDecimal saldo, BigDecimal pedido) {
super("Saldo insuficiente: saldo " + saldo + ", pedido " + pedido);
}
}
E o método novo da Conta:
public void sacar(BigDecimal valor) throws SaldoInsuficienteException {
validar(valor);
if (valor.compareTo(saldo) > 0) {
throw new SaldoInsuficienteException(saldo, valor);
}
saldo = saldo.subtract(valor);
}
- throws SaldoInsuficienteException
- Avisa que o saque pode falhar por falta de saldo. Quem chamar é obrigado a tratar ou repassar.
- valor.compareTo(saldo) > 0
- Com
BigDecimalnão se usa>.a.compareTo(b)devolve um número negativo (a menor), 0 (iguais) ou positivo (a maior). "> 0" = o valor pedido é maior que o saldo. - throw new SaldoInsuficienteException(saldo, valor)
- Lança antes de mexer no saldo: se falhar, a conta fica como estava.
- saldo.subtract(valor)
- Subtração, de novo guardando o resultado.
Dois testes novos no ContaTest:
@Test
void saqueDiminuiOSaldo() throws SaldoInsuficienteException {
ana.depositar(new BigDecimal("100.00"));
ana.sacar(new BigDecimal("30.25"));
assertEquals(new BigDecimal("69.75"), ana.getSaldo());
}
@Test
void saqueMaiorQueOSaldoLancaExcecao() {
ana.depositar(new BigDecimal("50.00"));
SaldoInsuficienteException erro = assertThrows(SaldoInsuficienteException.class,
() -> ana.sacar(new BigDecimal("80.00")));
assertEquals("Saldo insuficiente: saldo 50.00, pedido 80.00", erro.getMessage());
assertEquals(new BigDecimal("50.00"), ana.getSaldo());
}
O primeiro teste tem throws SaldoInsuficienteException porque chama sacar sem try; se a exceção vier, o JUnit marca o teste como erro. O segundo confere três coisas: que a exceção veio, que a mensagem está certa e que o saldo não mudou.
$ mvn test … [INFO] Running br.com.ana.banco.ContaTest [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.112 s -- in br.com.ana.banco.ContaTest … [INFO] BUILD SUCCESS
E se ninguém tratar a exceção? Um App com throws no main deixa ela subir até a JVM:
$ java -cp target/classes br.com.ana.banco.App Exception in thread "main" br.com.ana.banco.SaldoInsuficienteException: Saldo insuficiente: saldo 50.00, pedido 80.00 at br.com.ana.banco.Conta.sacar(Conta.java:29) at br.com.ana.banco.App.main(App.java:9)
Repare no nome completo da sua exceção, com o pacote, logo depois de Exception in thread "main".
Missão (etapa 4): o método sacar está vazio: o saldo nunca muda. Escreva-o: valide o valor, lance SaldoInsuficienteException se o valor for maior que o saldo (use compareTo) e subtraia com subtract.
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
Conta ana = new Conta("Ana");
ana.depositar(new BigDecimal("100.00"));
try {
ana.sacar(new BigDecimal("30.25"));
System.out.println("Saldo: " + ana.getSaldo());
ana.sacar(new BigDecimal("80.00"));
System.out.println("Saldo: " + ana.getSaldo());
} catch (SaldoInsuficienteException e) {
System.out.println("Erro: " + e.getMessage());
}
}
}
class Conta {
private final String titular;
private BigDecimal saldo = BigDecimal.ZERO;
Conta(String titular) {
this.titular = titular;
}
String getTitular() {
return titular;
}
BigDecimal getSaldo() {
return saldo;
}
void depositar(BigDecimal valor) {
validar(valor);
saldo = saldo.add(valor);
}
void sacar(BigDecimal valor) throws SaldoInsuficienteException {
// confira o saldo e lance SaldoInsuficienteException se faltar dinheiro
}
private static void validar(BigDecimal valor) {
if (valor == null || valor.signum() <= 0) {
throw new IllegalArgumentException("O valor deve ser positivo: " + valor);
}
}
}
class SaldoInsuficienteException extends Exception {
SaldoInsuficienteException(BigDecimal saldo, BigDecimal pedido) {
super("Saldo insuficiente: saldo " + saldo + ", pedido " + pedido);
}
}
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
Conta ana = new Conta("Ana");
ana.depositar(new BigDecimal("100.00"));
try {
ana.sacar(new BigDecimal("30.25"));
System.out.println("Saldo: " + ana.getSaldo());
ana.sacar(new BigDecimal("80.00"));
System.out.println("Saldo: " + ana.getSaldo());
} catch (SaldoInsuficienteException e) {
System.out.println("Erro: " + e.getMessage());
}
}
}
class Conta {
private final String titular;
private BigDecimal saldo = BigDecimal.ZERO;
Conta(String titular) {
this.titular = titular;
}
String getTitular() {
return titular;
}
BigDecimal getSaldo() {
return saldo;
}
void depositar(BigDecimal valor) {
validar(valor);
saldo = saldo.add(valor);
}
void sacar(BigDecimal valor) throws SaldoInsuficienteException {
validar(valor);
if (valor.compareTo(saldo) > 0) {
throw new SaldoInsuficienteException(saldo, valor);
}
saldo = saldo.subtract(valor);
}
private static void validar(BigDecimal valor) {
if (valor == null || valor.signum() <= 0) {
throw new IllegalArgumentException("O valor deve ser positivo: " + valor);
}
}
}
class SaldoInsuficienteException extends Exception {
SaldoInsuficienteException(BigDecimal saldo, BigDecimal pedido) {
super("Saldo insuficiente: saldo " + saldo + ", pedido " + pedido);
}
}
Etapa 5 · Transferência
Transferir é sacar de uma conta e depositar na outra. Como os dois métodos já existem e já têm as regras, a transferência fica com duas linhas:
public void transferir(Conta destino, BigDecimal valor) throws SaldoInsuficienteException {
this.sacar(valor);
destino.depositar(valor);
}
A ordem importa: primeiro o saque, que pode falhar. Se falhar, a exceção interrompe o método antes do depósito, e nenhum dinheiro "aparece" no destino. Guarde isso: a etapa 6 é sobre essa ordem. Os testes novos:
@Test
void transferenciaMoveODinheiro() throws SaldoInsuficienteException {
ana.depositar(new BigDecimal("1000.00"));
ana.transferir(bruno, new BigDecimal("250.75"));
assertEquals(new BigDecimal("749.25"), ana.getSaldo());
assertEquals(new BigDecimal("250.75"), bruno.getSaldo());
}
@Test
void transferenciaSemSaldoNaoMexeEmNenhumaConta() {
ana.depositar(new BigDecimal("30.00"));
assertThrows(SaldoInsuficienteException.class,
() -> ana.transferir(bruno, new BigDecimal("100.00")));
assertEquals(new BigDecimal("30.00"), ana.getSaldo());
assertEquals(BigDecimal.ZERO, bruno.getSaldo());
}
O App final (completo no fim da página) faz uma transferência e um saque recusado, agora tratado com try/catch:
$ mvn test … [INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0 … [INFO] BUILD SUCCESS $ java -cp target/classes br.com.ana.banco.App Ana: R$ 749.25 Bruno: R$ 250.75 Erro: Saldo insuficiente: saldo 250.75, pedido 300.00
Missão (etapa 5): escreva o transferir, reaproveitando sacar e depositar, para a Ana ficar com R$ 749.25 e o Bruno com R$ 250.75.
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) throws SaldoInsuficienteException {
Conta ana = new Conta("Ana");
Conta bruno = new Conta("Bruno");
ana.depositar(new BigDecimal("1000.00"));
ana.transferir(bruno, new BigDecimal("250.75"));
System.out.println(ana.getTitular() + ": R$ " + ana.getSaldo());
System.out.println(bruno.getTitular() + ": R$ " + bruno.getSaldo());
}
}
class Conta {
private final String titular;
private BigDecimal saldo = BigDecimal.ZERO;
Conta(String titular) {
this.titular = titular;
}
String getTitular() {
return titular;
}
BigDecimal getSaldo() {
return saldo;
}
void depositar(BigDecimal valor) {
validar(valor);
saldo = saldo.add(valor);
}
void sacar(BigDecimal valor) throws SaldoInsuficienteException {
validar(valor);
if (valor.compareTo(saldo) > 0) {
throw new SaldoInsuficienteException(saldo, valor);
}
saldo = saldo.subtract(valor);
}
void transferir(Conta destino, BigDecimal valor) throws SaldoInsuficienteException {
// tire o dinheiro desta conta e coloque no destino
}
private static void validar(BigDecimal valor) {
if (valor == null || valor.signum() <= 0) {
throw new IllegalArgumentException("O valor deve ser positivo: " + valor);
}
}
}
class SaldoInsuficienteException extends Exception {
SaldoInsuficienteException(BigDecimal saldo, BigDecimal pedido) {
super("Saldo insuficiente: saldo " + saldo + ", pedido " + pedido);
}
}
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) throws SaldoInsuficienteException {
Conta ana = new Conta("Ana");
Conta bruno = new Conta("Bruno");
ana.depositar(new BigDecimal("1000.00"));
ana.transferir(bruno, new BigDecimal("250.75"));
System.out.println(ana.getTitular() + ": R$ " + ana.getSaldo());
System.out.println(bruno.getTitular() + ": R$ " + bruno.getSaldo());
}
}
class Conta {
private final String titular;
private BigDecimal saldo = BigDecimal.ZERO;
Conta(String titular) {
this.titular = titular;
}
String getTitular() {
return titular;
}
BigDecimal getSaldo() {
return saldo;
}
void depositar(BigDecimal valor) {
validar(valor);
saldo = saldo.add(valor);
}
void sacar(BigDecimal valor) throws SaldoInsuficienteException {
validar(valor);
if (valor.compareTo(saldo) > 0) {
throw new SaldoInsuficienteException(saldo, valor);
}
saldo = saldo.subtract(valor);
}
void transferir(Conta destino, BigDecimal valor) throws SaldoInsuficienteException {
this.sacar(valor);
destino.depositar(valor);
}
private static void validar(BigDecimal valor) {
if (valor == null || valor.signum() <= 0) {
throw new IllegalArgumentException("O valor deve ser positivo: " + valor);
}
}
}
class SaldoInsuficienteException extends Exception {
SaldoInsuficienteException(BigDecimal saldo, BigDecimal pedido) {
super("Saldo insuficiente: saldo " + saldo + ", pedido " + pedido);
}
}
Etapa 6 · Uma falha de propósito
Para ver os testes trabalhando, a Ana vai "melhorar" o código e, sem perceber, quebrá-lo. Ela pensa: "na transferência, é mais simpático o dinheiro chegar primeiro", e inverte as linhas:
Este código tem um erro de propósito.
public void transferir(Conta destino, BigDecimal valor) throws SaldoInsuficienteException {
destino.depositar(valor);
this.sacar(valor);
}
O código compila, e o App mostra exatamente a mesma saída de antes (a transferência dele tem saldo). Sem testes, o defeito iria para produção. Com testes:
$ mvn test … « compilação igual à da etapa 3 [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running br.com.ana.banco.ContaTest [ERROR] Tests run: 7, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.123 s <<< FAILURE! -- in br.com.ana.banco.ContaTest [ERROR] br.com.ana.banco.ContaTest.transferenciaSemSaldoNaoMexeEmNenhumaConta -- Time elapsed: 0.068 s <<< FAILURE! org.opentest4j.AssertionFailedError: expected: <0> but was: <100.00> 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:182) at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177) at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1145) at br.com.ana.banco.ContaTest.transferenciaSemSaldoNaoMexeEmNenhumaConta(ContaTest.java:68) 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.transferenciaSemSaldoNaoMexeEmNenhumaConta:68 expected: <0> but was: <100.00> [INFO] [ERROR] Tests run: 7, Failures: 1, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [ERROR] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.974 s [INFO] Finished at: 2026-10-16T18:53:51-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/java/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] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
Como ler esse relatório, de cima para baixo:
| Linha | O que ela diz |
|---|---|
Tests run: 7, Failures: 1 | Sete testes rodaram; um falhou. Os outros seis continuam verdes. |
ContaTest.transferenciaSemSaldoNaoMexeEmNenhumaConta | Qual teste falhou. Um nome que descreve a regra ajuda muito agora: "transferência sem saldo não mexe em nenhuma conta". |
expected: <0> but was: <100.00> | O que deu errado: o saldo do Bruno devia ser 0, mas era 100.00. |
ContaTest.java:68 | Onde: a linha 68 do teste, o assertEquals(BigDecimal.ZERO, bruno.getSaldo()). |
BUILD FAILURE e as linhas [ERROR] do fim | O Maven parou por causa dos testes. As dicas (-e, -X, surefire-reports) servem para casos mais difíceis; aqui a mensagem já disse tudo. |
O diagnóstico: o Bruno recebeu R$ 100,00 antes de a Ana descobrir que não tinha saldo. O saque falhou, a exceção saiu do método… e o depósito já tinha acontecido. O banco criou dinheiro do nada. A correção é voltar à ordem certa (sacar primeiro):
public void transferir(Conta destino, BigDecimal valor) throws SaldoInsuficienteException {
this.sacar(valor);
destino.depositar(valor);
}
$ mvn test … [INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0 … [INFO] BUILD SUCCESS
O teste transferenciaSemSaldoNaoMexeEmNenhumaConta foi escrito na etapa 5, quando o código estava certo. Ele parecia inútil… até alguém mexer no código. Testes são uma rede de proteção para o futuro: rode mvn test antes de toda entrega. O mvn package, que gera o JAR, roda os testes e se recusa a empacotar com teste vermelho (veja Maven).
Missão (etapa 6): esta é a versão com o defeito, e o "teste caseiro" do main mostra FALHOU: Ana tem 30.00 e Bruno tem 100.00. Conserte o transferir para o teste mostrar OK.
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
Conta ana = new Conta("Ana");
Conta bruno = new Conta("Bruno");
ana.depositar(new BigDecimal("30.00"));
try {
ana.transferir(bruno, new BigDecimal("100.00"));
System.out.println("FALHOU: a transferência deveria ter sido recusada");
} catch (SaldoInsuficienteException e) {
System.out.println("Recusada: " + e.getMessage());
}
if (ana.getSaldo().equals(new BigDecimal("30.00")) && bruno.getSaldo().equals(BigDecimal.ZERO)) {
System.out.println("OK: transferência sem saldo não mexe em nenhuma conta");
} else {
System.out.println("FALHOU: Ana tem " + ana.getSaldo() + " e Bruno tem " + bruno.getSaldo());
}
}
}
class Conta {
private final String titular;
private BigDecimal saldo = BigDecimal.ZERO;
Conta(String titular) {
this.titular = titular;
}
String getTitular() {
return titular;
}
BigDecimal getSaldo() {
return saldo;
}
void depositar(BigDecimal valor) {
validar(valor);
saldo = saldo.add(valor);
}
void sacar(BigDecimal valor) throws SaldoInsuficienteException {
validar(valor);
if (valor.compareTo(saldo) > 0) {
throw new SaldoInsuficienteException(saldo, valor);
}
saldo = saldo.subtract(valor);
}
void transferir(Conta destino, BigDecimal valor) throws SaldoInsuficienteException {
destino.depositar(valor);
this.sacar(valor);
}
private static void validar(BigDecimal valor) {
if (valor == null || valor.signum() <= 0) {
throw new IllegalArgumentException("O valor deve ser positivo: " + valor);
}
}
}
class SaldoInsuficienteException extends Exception {
SaldoInsuficienteException(BigDecimal saldo, BigDecimal pedido) {
super("Saldo insuficiente: saldo " + saldo + ", pedido " + pedido);
}
}
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
Conta ana = new Conta("Ana");
Conta bruno = new Conta("Bruno");
ana.depositar(new BigDecimal("30.00"));
try {
ana.transferir(bruno, new BigDecimal("100.00"));
System.out.println("FALHOU: a transferência deveria ter sido recusada");
} catch (SaldoInsuficienteException e) {
System.out.println("Recusada: " + e.getMessage());
}
if (ana.getSaldo().equals(new BigDecimal("30.00")) && bruno.getSaldo().equals(BigDecimal.ZERO)) {
System.out.println("OK: transferência sem saldo não mexe em nenhuma conta");
} else {
System.out.println("FALHOU: Ana tem " + ana.getSaldo() + " e Bruno tem " + bruno.getSaldo());
}
}
}
class Conta {
private final String titular;
private BigDecimal saldo = BigDecimal.ZERO;
Conta(String titular) {
this.titular = titular;
}
String getTitular() {
return titular;
}
BigDecimal getSaldo() {
return saldo;
}
void depositar(BigDecimal valor) {
validar(valor);
saldo = saldo.add(valor);
}
void sacar(BigDecimal valor) throws SaldoInsuficienteException {
validar(valor);
if (valor.compareTo(saldo) > 0) {
throw new SaldoInsuficienteException(saldo, valor);
}
saldo = saldo.subtract(valor);
}
void transferir(Conta destino, BigDecimal valor) throws SaldoInsuficienteException {
this.sacar(valor);
destino.depositar(valor);
}
private static void validar(BigDecimal valor) {
if (valor == null || valor.signum() <= 0) {
throw new IllegalArgumentException("O valor deve ser positivo: " + valor);
}
}
}
class SaldoInsuficienteException extends Exception {
SaldoInsuficienteException(BigDecimal saldo, BigDecimal pedido) {
super("Saldo insuficiente: saldo " + saldo + ", pedido " + pedido);
}
}
O código completo
package br.com.ana.banco;
import java.math.BigDecimal;
public class Conta {
private final String titular;
private BigDecimal saldo = BigDecimal.ZERO;
public Conta(String titular) {
this.titular = titular;
}
public String getTitular() {
return titular;
}
public BigDecimal getSaldo() {
return saldo;
}
public void depositar(BigDecimal valor) {
validar(valor);
saldo = saldo.add(valor);
}
public void sacar(BigDecimal valor) throws SaldoInsuficienteException {
validar(valor);
if (valor.compareTo(saldo) > 0) {
throw new SaldoInsuficienteException(saldo, valor);
}
saldo = saldo.subtract(valor);
}
public void transferir(Conta destino, BigDecimal valor) throws SaldoInsuficienteException {
this.sacar(valor);
destino.depositar(valor);
}
private static void validar(BigDecimal valor) {
if (valor == null || valor.signum() <= 0) {
throw new IllegalArgumentException("O valor deve ser positivo: " + valor);
}
}
}
package br.com.ana.banco;
import java.math.BigDecimal;
public class App {
public static void main(String[] args) throws SaldoInsuficienteException {
Conta ana = new Conta("Ana");
Conta bruno = new Conta("Bruno");
ana.depositar(new BigDecimal("1000.00"));
ana.transferir(bruno, new BigDecimal("250.75"));
System.out.println(ana.getTitular() + ": R$ " + ana.getSaldo());
System.out.println(bruno.getTitular() + ": R$ " + bruno.getSaldo());
try {
bruno.sacar(new BigDecimal("300.00"));
} catch (SaldoInsuficienteException e) {
System.out.println("Erro: " + e.getMessage());
}
}
}
package br.com.ana.banco;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.math.BigDecimal;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ContaTest {
private Conta ana;
private Conta bruno;
@BeforeEach
void preparar() {
ana = new Conta("Ana");
bruno = new Conta("Bruno");
}
@Test
void depositoAumentaOSaldo() {
ana.depositar(new BigDecimal("100.50"));
assertEquals(new BigDecimal("100.50"), ana.getSaldo());
}
@Test
void centavosSomamSemErro() {
ana.depositar(new BigDecimal("0.10"));
ana.depositar(new BigDecimal("0.20"));
assertEquals(new BigDecimal("0.30"), ana.getSaldo());
}
@Test
void depositoNegativoEhRecusado() {
assertThrows(IllegalArgumentException.class, () -> ana.depositar(new BigDecimal("-5.00")));
}
@Test
void saqueDiminuiOSaldo() throws SaldoInsuficienteException {
ana.depositar(new BigDecimal("100.00"));
ana.sacar(new BigDecimal("30.25"));
assertEquals(new BigDecimal("69.75"), ana.getSaldo());
}
@Test
void saqueMaiorQueOSaldoLancaExcecao() {
ana.depositar(new BigDecimal("50.00"));
SaldoInsuficienteException erro = assertThrows(SaldoInsuficienteException.class,
() -> ana.sacar(new BigDecimal("80.00")));
assertEquals("Saldo insuficiente: saldo 50.00, pedido 80.00", erro.getMessage());
assertEquals(new BigDecimal("50.00"), ana.getSaldo());
}
@Test
void transferenciaMoveODinheiro() throws SaldoInsuficienteException {
ana.depositar(new BigDecimal("1000.00"));
ana.transferir(bruno, new BigDecimal("250.75"));
assertEquals(new BigDecimal("749.25"), ana.getSaldo());
assertEquals(new BigDecimal("250.75"), bruno.getSaldo());
}
@Test
void transferenciaSemSaldoNaoMexeEmNenhumaConta() {
ana.depositar(new BigDecimal("30.00"));
assertThrows(SaldoInsuficienteException.class,
() -> ana.transferir(bruno, new BigDecimal("100.00")));
assertEquals(new BigDecimal("30.00"), ana.getSaldo());
assertEquals(BigDecimal.ZERO, bruno.getSaldo());
}
}
O pom.xml está na etapa 1 e a SaldoInsuficienteException na etapa 4.
Desafios extras
- Extrato. Guarde cada operação numa
List<String>("Depósito: 100.00","Saque: 30.25") e criegetExtrato(). Teste que depois de um depósito e um saque o extrato tem 2 linhas. - Transferir para si mesma.
ana.transferir(ana, …)deveria ser recusado comIllegalArgumentException. Escreva o teste primeiro, veja falhar, depois implemente (isso é o TDD da aula de JUnit). - Limite de cheque especial. Uma
ContaEspecial extends Contaque pode ficar negativa até um limite (veja Herança). O que precisa mudar naContapara isso ser possível? - Duas casas sempre. Faça o depósito arredondar para 2 casas com
valor.setScale(2, RoundingMode.HALF_EVEN)e teste quedepositar(new BigDecimal("10"))deixa o saldo em10.00.
Exercícios
1. Por que o saldo é BigDecimal e não double?
2. O que acontece com saldo.add(valor); sozinho, sem saldo = na frente?
3. O teste diz expected: <100> but was: <100.00>. O que houve?
4. Por que transferir chama sacar antes de depositar?
Complete o comando que compila e roda todos os testes do projeto:
Resumo
- Projeto Maven: código em
src/main/java, testes emsrc/test/java, JUnit nopom.xmlcom escopotest. - Dinheiro é
BigDecimalcriado de texto; as contas são métodos (add,subtract,compareTo) e o resultado precisa ser guardado. SaldoInsuficienteExceptioné checked: falta de saldo é uma situação esperada, que quem chama deve tratar.@Test,@BeforeEach,assertEquals(esperado, obtido)eassertThrowscobrem quase tudo.- Um teste vermelho diz qual regra quebrou, o valor esperado, o obtido e a linha. Rode
mvn testantes de entregar.
Próximo projeto: Tarefas com Maven, um programa de linha de comando empacotado num JAR.