Pular para o conteúdo

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

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

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.

Exceção lançada em lerIdade: se um catch combina, ele trata e o fluxo segue; se nenhum combina, o finally roda antes de a exceção sair para o chamador.
figura 1 · Caminho de uma exceção por try, catch e finally

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.

Um teste prepara a entrada, chama a função e confere o resultado com uma asserção; se a conferência falha, o relatório mostra o esperado e o obtido.
figura 2 · Um teste faz três passos: preparar, agir e conferir

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.

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.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?