Trilha de Kotlin · parte 7 de 59
Exceções e primeiro teste automatizado
Veja como uma exceção interrompe o fluxo, como tratá-la com try, catch e finally e como escrever o primeiro teste com kotlin.test.
iniciantePublicada em · 13 min de leitura
Exceções e primeiro teste automatizado
Imagine um garçom que, ao descobrir que a cozinha acabou o frango, não traz outro prato no lugar. Ele volta à mesa e avisa, e a refeição para até alguém decidir o que fazer. Uma é esse aviso. Quando um trecho de código não consegue cumprir o combinado, ele interrompe o caminho normal e entrega um objeto que descreve o problema.
A segunda metade da aula ensina a conferir o código com um teste automatizado. Os dois assuntos andam juntos porque o jeito mais útil de aprender a testar é testar um erro.
o mapa da aula
A aula segue a ordem de sempre: o que é uma exceção, throw, try, catch e finally, try como expressão, as exceções do Kotlin, a comparação com Java, use, erros comuns, o primeiro teste, um exercício e o glossário.
1. o que é uma exceção e por que ela existe
Uma função que não consegue cumprir o trabalho precisa avisar de algum jeito. Devolver um valor especial, como -1 ou null, depende de quem chama lembrar de conferir, e esse lembrete é esquecido com facilidade. A exceção separa o caminho feliz do erro: o erro sobe pela cadeia de chamadas até alguém tratá-lo, e, se ninguém tratar, o programa termina com um rastro. Com "abc".toInt(), a biblioteca padrão faz isso:
fun lerNumero(texto: String): Int {
return texto.toInt()
}
fun main() {
println("antes")
val n = lerNumero("abc")
println("depois: $n")
}
antes
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
...
at MainKt.lerNumero(Main.kt:2)
at MainKt.main(Main.kt:7)
A linha antes saiu porque foi impressa antes do erro. O rastro mostra a cadeia: lerNumero na linha 2, chamada por main na linha 7. Cortei as linhas internas do JDK. Quando um programa cai, leia o rastro primeiro.
2. lançar com throw
throw lança uma exceção: throw IllegalArgumentException("idade negativa"). Ele tem tipo Nothing, o que quer dizer que o trecho nunca termina normalmente. Você escreve throw nos pontos em que a função descobre que não pode continuar.
3. try, catch e finally
O try marca o código que pode falhar. Cada catch declara um tipo de exceção que sabe tratar, e o finally roda sempre, com ou sem erro. Se o try termina sem erro, os catch são ignorados. Se lança, a execução pula para o primeiro catch cujo tipo combina, e o finally roda depois. Dividir por zero lança ArithmeticException, e o programa abaixo mostra os dois casos:
fun dividir(a: Int, b: Int): Int {
println(" dividir($a, $b) começou")
val resultado = a / b
println(" dividir($a, $b) terminou")
return resultado
}
fun calcular(a: Int, b: Int) {
println("início de calcular($a, $b)")
try {
val r = dividir(a, b)
println("resultado: $r")
} catch (e: ArithmeticException) {
println("não dá para dividir: ${e.message}")
} finally {
println("finally sempre roda")
}
println("fim de calcular($a, $b)")
}
fun main() {
calcular(10, 2)
calcular(10, 0)
}
início de calcular(10, 2)
dividir(10, 2) começou
dividir(10, 2) terminou
resultado: 5
finally sempre roda
fim de calcular(10, 2)
início de calcular(10, 0)
dividir(10, 0) começou
não dá para dividir: / by zero
finally sempre roda
fim de calcular(10, 0)
Na segunda chamada, a linha dividir(10, 0) terminou nunca aparece, porque a divisão lançou antes dela. O finally roda nos dois casos.
Quando nenhum catch combina, a exceção deixa o bloco try, o finally roda e só então ela sobe para quem chamou. A figura mostra os três destinos de uma chamada como lerIdade: sem erro, erro tratado e erro que sobe. Nos três caminhos, o finally roda antes de a execução deixar o bloco.
Leia a figura de cima para baixo. O try chama lerIdade, e o throw lá dentro leva ao losango, que pergunta se algum catch combina. Se combina, o caminho verde trata o erro e segue. Se não combina, o caminho rosa vai ao finally, que roda antes de sair. Dos dois caminhos com erro, só o que sobe pode terminar o programa, e isso acontece quando nenhum chamador captura a exceção.
4. try como expressão
Como if e when, o try produz um valor: o da última expressão do bloco que terminou. O finally não altera esse valor.
fun main() {
val a = try { "42".toInt() } catch (e: NumberFormatException) { -1 }
val b = try { "quarenta".toInt() } catch (e: NumberFormatException) { -1 }
println("a = $a, b = $b")
}
a = 42, b = -1
5. exceções que o Kotlin lança com frequência
IllegalArgumentException diz que um argumento não serve. IllegalStateException diz que o estado do objeto não permite a operação. NoSuchElementException aparece ao pedir um elemento que não existe, como o primeiro de uma lista vazia. NumberFormatException aparece quando um texto não vira número, e herda de IllegalArgumentException.
Para lançá-las com mensagem clara, use require em argumentos, check em estados e error em trechos que não deveriam ser alcançados. O programa mostra cada uma:
fun tentar(nome: String, bloco: () -> Unit) {
try {
bloco()
println("$nome: sem erro")
} catch (e: Exception) {
// catch genérico só neste programa, que quer imprimir qualquer erro
println("$nome: ${e::class.simpleName}: ${e.message}")
}
}
fun validarIdade(idade: Int) {
require(idade >= 0) { "idade não pode ser negativa: $idade" }
}
fun abrirCaixa(aberta: Boolean) {
check(aberta) { "caixa precisa estar aberta" }
}
fun main() {
tentar("require") { validarIdade(-1) }
tentar("check") { abrirCaixa(false) }
tentar("error") { error("pedido sem itens") }
tentar("first") { emptyList<Int>().first() }
tentar("toInt") { "abc".toInt() }
println("NumberFormatException é IllegalArgumentException? ${NumberFormatException() is IllegalArgumentException}")
}
require: IllegalArgumentException: idade não pode ser negativa: -1
check: IllegalStateException: caixa precisa estar aberta
error: IllegalStateException: pedido sem itens
first: NoSuchElementException: List is empty.
toInt: NumberFormatException: For input string: "abc"
NumberFormatException é IllegalArgumentException? true
O catch (e: Exception) da função tentar existe só para imprimir o erro nesta demonstração. A seção 8 mostra por que esse padrão é perigoso no código de verdade.
6. kotlin não tem exceções verificadas
Em Java, exceções verificadas () são as que o compilador obriga você a capturar com try e catch ou a declarar com throws na assinatura. Kotlin não cobra isso: todas as exceções são não verificadas. O código abaixo lê um arquivo inexistente sem try nem throws, e compila.
import java.io.FileReader
fun lerArquivo(caminho: String): String {
return FileReader(caminho).use { it.readText() }
}
fun main() {
println("antes")
println(lerArquivo("nao-existe.txt"))
}
antes
Exception in thread "main" java.io.FileNotFoundException: nao-existe.txt (No such file or directory)
at java.base/java.io.FileInputStream.open0(Native Method)
...
at java.base/java.io.FileReader.<init>(FileReader.java:60)
at MainKt.lerArquivo(Main.kt:4)
at MainKt.main(Main.kt:9)
Em Java, a mesma chamada sem try não compila. O javac 25 respondeu unreported exception FileNotFoundException; must be caught or declared to be thrown, ou seja, a exceção precisa ser capturada ou declarada. O trecho em Java não está nesta nota. A liberdade do Kotlin tem um preço: o compilador não avisa quais erros uma função pode lançar, então isso precisa estar na documentação. Quando código Kotlin é chamado por Java, a anotação @Throws faz esse aviso.
7. fechar recursos com use
Um recurso, como um arquivo ou uma conexão, precisa ser fechado mesmo quando algo falha. A função use executa o bloco e fecha o recurso ao final, com ou sem exceção. Ela vale para objetos AutoCloseable, que têm o método close() da JVM, e para Closeable, do pacote java.io.
class Conexao(private val nome: String) : AutoCloseable {
init {
println("abrindo $nome")
}
fun enviar(mensagem: String) {
println("enviando '$mensagem' por $nome")
check(mensagem.isNotBlank()) { "mensagem vazia" }
}
override fun close() {
println("fechando $nome")
}
}
fun main() {
Conexao("banco").use { c ->
c.enviar("oi")
}
try {
Conexao("api").use { c ->
c.enviar(" ")
}
} catch (e: IllegalStateException) {
println("erro capturado: ${e.message}")
}
}
abrindo banco
enviando 'oi' por banco
fechando banco
abrindo api
enviando ' ' por api
fechando api
erro capturado: mensagem vazia
Em api, fechando api vem antes de erro capturado: o use fecha o recurso antes de a exceção chegar ao catch.
Pergunta de entrevistaKotlin tem exceções verificadas? Quais as consequências disso?
Não tem. Em Java, exceções verificadas são aquelas que o compilador obriga a capturar com try e catch ou a declarar com throws. Em Kotlin todas as exceções são não verificadas. O exemplo da aula lê um arquivo inexistente sem try nem throws e compila, enquanto em Java a mesma chamada não compilaria. Isso reduz código repetitivo. O preço é que o compilador não avisa quais erros uma função pode lançar, então essa informação precisa estar na documentação, e eu preciso decidir de forma consciente onde capturar cada exceção. Quando o erro faz parte do fluxo esperado, modelar o resultado de forma explícita deixa o contrato da função mais visível para quem a chama.
8. erros comuns e boas práticas
catch genérico e exceção engolida
Um catch (e: Exception) pega também os erros que você não previu, inclusive bugs na própria lógica. Pior é o catch que engole o erro e segue sem rastro. No exemplo, Z9 não existe na tabela, mas o preço sai zero e ninguém é avisado.
fun precoDoItem(codigo: String): Int {
return try {
when (codigo) {
"A1" -> 10
"B2" -> 25
else -> throw IllegalArgumentException("código desconhecido: $codigo")
}
} catch (e: Exception) {
0
}
}
fun main() {
println("A1 custa ${precoDoItem("A1")}")
println("Z9 custa ${precoDoItem("Z9")}")
}
A1 custa 10
Z9 custa 0
Capture só o tipo que você sabe tratar, e deixe os outros subirem. Quem captura deve registrar, converter ou recuperar. E não use exceção para o caso normal: uma busca sem resultado costuma devolver null ou uma lista vazia.
Pergunta de entrevistaComo garantir que um recurso seja fechado mesmo quando ocorre uma exceção em Kotlin?
Uso a função use. Ela executa o bloco e fecha o recurso ao final, com ou sem exceção, e vale para objetos AutoCloseable, que têm o método close da JVM, e para Closeable, do pacote java.io. É o equivalente ao try com recursos do Java. Na aula, a saída mostra o recurso sendo fechado antes de a exceção chegar ao catch, o que garante que nada fica aberto quando o erro é tratado. O erro comum é abrir um recurso e fechá-lo manualmente no fim do bloco, porque uma exceção no meio do caminho pula o fechamento e deixa o arquivo ou a conexão abertos.
9. o primeiro teste automatizado
por que testar
Rodar o programa à mão funciona uma vez. Um teste guarda a conferência em código e avisa quando o resultado muda.
preparar, agir e conferir
Todo teste tem três passos. Preparar monta a entrada, agir chama a função e conferir compara o resultado com uma , a comparação que derruba o teste quando não bate.
A conferência é um losango porque decide entre dois caminhos: o verde termina num relatório sem falhas, e o vermelho, na mensagem com o valor esperado e o obtido.
kotlin.test e JUnit 5
O projeto é o da aula de Ambiente e primeiro programa: kotlin("test") como dependência de teste e useJUnitPlatform(). A anotação @Test que você importa de kotlin.test vira org.junit.jupiter.api.Test no bytecode. Conferi isso no bytecode, porque a página de JUnit do Kotlin não descreve esse mapeamento.
o primeiro teste: validar a idade
A função lerIdade remove os espaços, converte o texto em número e recusa idades fora de 0 a 130.
fun lerIdade(texto: String): Int {
val idade = texto.trim().toInt()
require(idade in 0..130) { "idade fora da faixa: $idade" }
return idade
}
Os testes ficam em src/test/kotlin, e cada método marcado com @Test é um teste.
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class IdadeTest {
@Test
fun aceitaIdadeValida() {
val texto = "30"
val idade = lerIdade(texto)
assertEquals(30, idade)
}
@Test
fun recusaIdadeNegativa() {
val erro = assertFailsWith<IllegalArgumentException> {
lerIdade("-1")
}
assertEquals("idade fora da faixa: -1", erro.message)
}
@Test
fun recusaTextoQueNaoEhNumero() {
assertFailsWith<NumberFormatException> {
lerIdade("abc")
}
}
}
Em assertEquals, o valor esperado vem primeiro e o obtido depois, o que importa para ler a falha. Com ./gradlew test, o resultado é:
> Task :test
BUILD SUCCESSFUL in 7s
ler o relatório de falha
Troque o 30 por 31 na conferência do primeiro teste e rode de novo. Fiz essa troca numa cópia, com um arquivo de um único método:
> Task :test FAILED
IdadeErradoTest > aceitaIdadeValida() FAILED
org.opentest4j.AssertionFailedError at IdadeErradoTest.kt:8
1 test completed, 1 failed
O console mostra o nome do teste e a linha da asserção. A mensagem completa fica no XML em build/test-results/test, no atributo message: expected: <31> but was: <30>.
testar um erro com assertFailsWith
assertFailsWith<T> { ... } passa se o bloco lançar uma exceção do tipo T, e devolve a exceção para você conferir a mensagem. Subclasses são aceitas: assertFailsWith<IllegalArgumentException> { lerIdade("abc") } passa, porque NumberFormatException herda de IllegalArgumentException. Se o bloco não lançar, o teste falha. Numa variante que chamava lerIdade("30") dentro do bloco, a mensagem real foi esta, sem tradução:
Expected an exception of class java.lang.IllegalArgumentException to be thrown, but was completed successfully with the result: <30>.
erros comuns em testes
O primeiro erro é o teste sem asserção. Ele chama a função e não confere nada, então passa mesmo com a função errada. Num exemplo fora desta nota, lerIdadeQuebrada devolve -999 de propósito, e um teste que só a chama termina em BUILD SUCCESSFUL.
O segundo é o teste que depende de ordem. Os dois métodos abaixo compartilham o objeto Estado. O bComecaEmZero só passa se o outro não tiver rodado antes. A anotação @TestMethodOrder força a ordem alfabética apenas para mostrar a falha.
import kotlin.test.Test
import kotlin.test.assertEquals
import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.TestMethodOrder
object Estado {
var valor = 0
}
@TestMethodOrder(MethodOrderer.MethodName::class)
class OrdemForcadaTest {
@Test
fun aIncrementaUmaVez() {
Estado.valor++
assertEquals(1, Estado.valor)
}
@Test
fun bComecaEmZero() {
assertEquals(0, Estado.valor)
}
}
Com a classe inteira, o Gradle mostrou:
OrdemForcadaTest > bComecaEmZero() FAILED
org.opentest4j.AssertionFailedError at OrdemForcadaTest.kt:20
Rodado sozinho, com --tests "OrdemForcadaTest.bComecaEmZero", o método passa. Em código real, nenhum teste deve depender da ordem.
10. exercício: saque com dois erros
Escreva sacar(saldo, valor), que devolve o novo saldo. Valor zero ou negativo lança IllegalArgumentException, e saque acima do saldo lança IllegalStateException. Escreva três testes: o caso feliz, o valor zero e o saque excessivo. Tente antes de ver a resposta.
fun sacar(saldo: Int, valor: Int): Int {
require(valor > 0) { "valor do saque precisa ser positivo: $valor" }
check(valor <= saldo) { "saldo insuficiente: saldo $saldo, saque $valor" }
return saldo - valor
}
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class SaqueTest {
@Test
fun saqueComSaldoSuficienteDevolveNovoSaldo() {
assertEquals(70, sacar(100, 30))
}
@Test
fun saqueDeValorZeroEhRecusado() {
val erro = assertFailsWith<IllegalArgumentException> {
sacar(100, 0)
}
assertEquals("valor do saque precisa ser positivo: 0", erro.message)
}
@Test
fun saqueMaiorQueSaldoEhRecusado() {
val erro = assertFailsWith<IllegalStateException> {
sacar(100, 150)
}
assertEquals("saldo insuficiente: saldo 100, saque 150", erro.message)
}
}
Os três testes passaram. O relatório XML traz tests="3", failures="0" e errors="0".
Segundo exercício, sem código: troque require por check dentro de sacar, na linha do valor zero. Então sacar(100, 0) lança IllegalStateException, e o teste saqueDeValorZeroEhRecusado falha, porque espera IllegalArgumentException. Não rodei essa variante; a conclusão segue da hierarquia de classes.
A fase 2 da trilha trata de POO em Kotlin, com classes, objetos e herança.
11. glossário
- Exceção: objeto que descreve um erro e interrompe o caminho normal até alguém tratá-lo.
- Exceção verificada (checked): exceção que o compilador de Java obriga a tratar ou declarar. Kotlin não tem.
- Asserção: comparação que derruba o teste quando o resultado não bate com o esperado.
Fontes
Consultadas em 04/11/2026.
- Kotlin, exceções (throw, try, catch, finally, try como expressão, checked exceptions e @Throws): https://kotlinlang.org/docs/exceptions.html
- Kotlin, testes com JUnit em projetos JVM: https://kotlinlang.org/docs/jvm-test-using-junit.html. A página não explica o mapeamento de
kotlin.test.Test, conferido no bytecode. - Kotlin, require: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/require.html
- Kotlin, check: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/check.html
- Kotlin, error: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/error.html
- Kotlin, use para Closeable: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/use.html
- Kotlin, use para AutoCloseable: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/use.html
- Kotlin, assertFailsWith: https://kotlinlang.org/api/core/kotlin-test/kotlin.test/assert-fails-with.html
Os trechos rodaram no projeto da aula de Ambiente e primeiro programa, com Kotlin 2.4.21, Gradle 9.7.0 pelo wrapper e OpenJDK 25; a comparação com Java usou o javac 25. Os menus do IntelliJ não foram verificados, e a aula usa o terminal.