Как правильно тестировать исключения в Kotlin с JUnit?
Идиоматичный способ: 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("нулем"))
}
Лучшие практики
- Тестируйте только одно исключение за тест — каждый тест должен проверять один сценарий ошибки.
- Используйте осмысленные имена тестов — в Kotlin можно использовать пробелы и обратные кавычки:
`division by zero throws exception`. - Проверяйте сообщение, если оно является частью контракта — это помогает отлавливать регрессии в тексте ошибок.
- Избегайте тестирования стандартных исключений 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 отличный выбор.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги