Avatar ·

Как правильно тестировать исключения в Kotlin с JUnit?

Всем привет! Я новичок в Kotlin и пытаюсь написать юнит-тесты для своего проекта. Раньше писал на Java, где для проверки исключений использовал `@Test(expected = ...)` или блоки `try-catch`. Но в Kotlin я столкнулся с проблемой — код стал менее читаемым, и я не уверен, что делаю всё правильно. У меня есть функция `divide(a: Int, b: Int): Int`, которая выбрасывает `IllegalArgumentException`, если делитель равен нулю. Вот как я пробовал тестировать: ```kotlin @Test fun testDivisionByZero() { try { divide(10, 0) fail("Exception expected") } catch (e: IllegalArgumentException) { // success } } ``` Это работает, но выглядит громоздко. Я слышал, что в Kotlin есть более современные подходы, например, с использованием `assertThrows` из JUnit 5. Но я не уверен, как правильно их применять, и какие вообще best practices существуют для Kotlin. Подскажите, пожалуйста: 1. Какой способ считается наиболее идиоматичным для Kotlin? 2. Есть ли какие-то библиотеки, которые упрощают тестирование исключений? 3. Как проверить не только сам факт исключения, но и его сообщение? Заранее спасибо за помощь! Буду рад примерам кода. <<>>

Идиоматичный способ: JUnit 5 и `assertThrows`

В Kotlin наиболее правильным и читаемым подходом является использование assertThrows из JUnit 5. Этот метод принимает ожидаемый класс исключения и лямбду с кодом, который должен его выбросить. Возвращаемое значение — само исключение, что позволяет проверить его сообщение или другие свойства.

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

class CalculatorTest {
    
    @Test
    fun `division by zero throws exception`() {
        val exception = assertThrows<IllegalArgumentException> {
            divide(10, 0)
        }
        assertEquals("Делитель не может быть нулем", exception.message)
    }
    
    private fun divide(a: Int, b: Int): Int {
        require(b != 0) { "Делитель не может быть нулем" }
        return a / b
    }
}

Почему это лучше?

  • Лаконичность — не нужно писать блоки try-catch и явно вызывать fail().
  • Типобезопасность — компилятор проверяет, что ожидаемое исключение действительно выбрасывается.
  • Читаемость — код сразу говорит о намерении: "я ожидаю, что здесь вылетит исключение".
  • Гибкость — можно проверить сообщение, cause или другие поля исключения.

Альтернативы для JUnit 4

Если вы вынуждены использовать JUnit 4 (например, в легаси-проекте), есть два варианта:

1. Использовать `@Test(expected = ...)` — простой, но не позволяет проверить сообщение:

@Test(expected = IllegalArgumentException::class)
fun `division by zero`() {
    divide(10, 0)
}

2. Использовать `ExpectedException` правило — более гибко, но требует дополнительного кода:

@Rule
@JvmField
val expectedException: ExpectedException = ExpectedException.none()

@Test
fun `division by zero with message check`() {
    expectedException.expect(IllegalArgumentException::class.java)
    expectedException.expectMessage("Делитель не может быть нулем")
    divide(10, 0)
}

Библиотеки для Kotlin

Существуют специализированные библиотеки, которые делают тестирование исключений ещё удобнее:

  • Kotest — предоставляет функцию shouldThrow:
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe

@Test
fun `kotest example`() {
    val exception = shouldThrow<IllegalArgumentException> {
        divide(10, 0)
    }
    exception.message shouldBe "Делитель не может быть нулем"
}
  • Hamcrest с Kotlin — можно комбинировать с матчерами:
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.*

@Test
fun `hamcrest example`() {
    val exception = assertThrows<IllegalArgumentException> {
        divide(10, 0)
    }
    assertThat(exception.message, containsString("нулем"))
}

Лучшие практики

  1. Тестируйте только одно исключение за тест — каждый тест должен проверять один сценарий ошибки.
  2. Используйте осмысленные имена тестов — в Kotlin можно использовать пробелы и обратные кавычки: `division by zero throws exception`.
  3. Проверяйте сообщение, если оно является частью контракта — это помогает отлавливать регрессии в тексте ошибок.
  4. Избегайте тестирования стандартных исключений JVM — например, NullPointerException лучше предотвращать через requireNotNull или элвис-оператор.

Таким образом, для Kotlin рекомендую использовать assertThrows из JUnit 5 или shouldThrow из Kotest. Это современно, читаемо и соответствует идиоматике языка.

<<>>

Альтернативный подход: Kotest и стиль "String Spec"

Если вы готовы перейти на более "котлиновский" тестовый фреймворк, Kotest предлагает не только удобные функции для проверки исключений, но и альтернативные стили описания тестов. Один из самых популярных — String Spec, где тесты выглядят как обычный текст.

import io.kotest.core.spec.style.StringSpec
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe

class CalculatorTest : StringSpec({
    "деление на ноль должно выбросить IllegalArgumentException" {
        val exception = shouldThrow<IllegalArgumentException> {
            divide(10, 0)
        }
        exception.message shouldBe "Делитель не может быть нулем"
    }
    
    "деление на ноль с другим сообщением" {
        shouldThrow<IllegalArgumentException> {
            divide(10, 0)
        }.message shouldBe "Делитель не может быть нулем"
    }
    
    private fun divide(a: Int, b: Int): Int {
        require(b != 0) { "Делитель не может быть нулем" }
        return a / b
    }
})

Преимущества этого подхода:

  • Читаемость — имена тестов пишутся на естественном языке без кавычек и аннотаций.
  • Лаконичность — можно объединить проверку исключения и его сообщения в одну цепочку.
  • Гибкость — Kotest поддерживает множество стилей (FunSpec, DescribeSpec, BehaviorSpec и др.) под разные предпочтения.

Когда это стоит использовать?

Если ваш проект уже использует Kotest или вы хотите максимально приблизить тесты к естественному языку. Для небольших проектов или команд, привыкших к JUnit, первый ответ будет более привычным. Но если вы пишете на Kotlin и хотите получить максимум от языка — Kotest отличный выбор.