Kotest для Kotlin: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в Kotest: что это и зачем нужно

Kotest (ранее известный как KotlinTest) — это мощная и гибкая библиотека для написания тестов на языке Kotlin. Она предоставляет разработчикам богатый набор инструментов для модульного, интеграционного и функционального тестирования. В отличие от стандартного JUnit, Kotest предлагает множество стилей написания тестов (от классического "xUnit" до Behavior-Driven Development), встроенную поддержку property-based testing, мощные матчеры (assertions) и интеграцию с популярными фреймворками, такими как Spring Boot, MockK и Arrow.

Зачем нужен Kotest? В современной разработке на Kotlin тестирование — это не просто проверка кода, а фундамент качества. Kotest решает несколько ключевых задач:

  • Гибкость стилей: Вы можете писать тесты в стиле "String Spec", "Fun Spec", "Behavior Spec" и других, выбирая наиболее удобный для вашей команды.
  • Мощные матчеры: Встроенные функции проверки (shouldBe, shouldContain, shouldThrow) делают код тестов читаемым и лаконичным.
  • Property-based testing: Позволяет генерировать тысячи случайных тестовых данных, проверяя инварианты вашего кода, а не только конкретные значения.
  • Интеграция с корутинами: Kotest отлично работает с корутинами Kotlin, позволяя тестировать асинхронный код без лишнего boilerplate.
  • Расширяемость: Вы можете создавать собственные матчеры, слушатели и расширения.

Установка Kotest

Kotest распространяется через Maven Central. Для установки вам нужно добавить зависимости в файл build.gradle.kts (для Gradle) или pom.xml (для Maven).

Установка через Gradle (build.gradle.kts)

// В секции dependencies
dependencies {
    // Основной модуль (включает базовые матчеры и стили)
    testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
    // Модуль с property-based testing
    testImplementation("io.kotest:kotest-property:5.9.1")
    // Модуль для работы с фреймворками (например, Spring)
    testImplementation("io.kotest:kotest-extensions-spring:5.9.1")
}

Установка через Maven (pom.xml)

<dependency>
    <groupId>io.kotest</groupId>
    <artifactId>kotest-runner-junit5</artifactId>
    <version>5.9.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.kotest</groupId>
    <artifactId>kotest-property</artifactId>
    <version>5.9.1</version>
    <scope>test</scope>
</dependency>

Примечание: Версия 5.9.1 актуальна на момент написания статьи. Всегда проверяйте последнюю версию на MVNRepository.

Быстрый старт с примером кода

Давайте напишем простой тест для класса-калькулятора, используя стиль "Fun Spec" — один из самых популярных и интуитивно понятных.

// Класс, который будем тестировать
class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    fun divide(a: Int, b: Int): Int {
        if (b == 0) throw ArithmeticException("Деление на ноль")
        return a / b
    }
}

// Тестовый класс
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldThrow

class CalculatorTest : FunSpec({
    // Инициализация тестируемого объекта
    val calculator = Calculator()

    test("сложение двух положительных чисел") {
        val result = calculator.add(2, 3)
        result shouldBe 5
    }

    test("деление на ноль должно выбросить исключение") {
        shouldThrow<ArithmeticException> {
            calculator.divide(10, 0)
        }
    }
})

Этот пример демонстрирует базовую структуру: мы создаем класс, наследующийся от FunSpec, и внутри лямбды пишем тесты с помощью функции test. Для проверки используем матчеры shouldBe и shouldThrow.

Основные методы и функции Kotest

1. shouldBe — проверка равенства

Что делает: Проверяет, что фактическое значение равно ожидаемому. Использует метод equals объекта.

Параметры: Принимает ожидаемое значение любого типа.

import io.kotest.matchers.shouldBe

val name = "Kotlin"
name shouldBe "Kotlin" // Тест пройден

val list = listOf(1, 2, 3)
list shouldBe listOf(1, 2, 3) // Тест пройден

2. shouldNotBe — проверка неравенства

Что делает: Проверяет, что фактическое значение не равно ожидаемому.

Параметры: Принимает значение, с которым не должно совпадать.

import io.kotest.matchers.shouldNotBe

val age = 25
age shouldNotBe 30 // Тест пройден

3. shouldThrow — проверка исключений

Что делает: Проверяет, что блок кода выбрасывает исключение указанного типа. Возвращает объект исключения для дальнейших проверок.

Параметры: Тип исключения (например, ArithmeticException::class).

import io.kotest.matchers.shouldThrow

shouldThrow<IllegalArgumentException> {
    require(false) { "Ошибка!" }
}

// Можно проверить сообщение исключения
val exception = shouldThrow<ArithmeticException> {
    throw ArithmeticException("Деление на ноль")
}
exception.message shouldBe "Деление на ноль"

4. shouldContain — проверка вхождения в коллекцию

Что делает: Проверяет, что коллекция содержит указанный элемент.

Параметры: Элемент, который должен присутствовать в коллекции.

import io.kotest.matchers.collections.shouldContain

val fruits = listOf("apple", "banana", "orange")
fruits shouldContain "banana" // Тест пройден

5. shouldHaveSize — проверка размера коллекции

Что делает: Проверяет, что коллекция имеет ожидаемый размер.

Параметры: Ожидаемый размер (Int).

import io.kotest.matchers.collections.shouldHaveSize

val numbers = listOf(1, 2, 3, 4, 5)
numbers shouldHaveSize 5 // Тест пройден

6. shouldBeEmpty — проверка пустоты

Что делает: Проверяет, что коллекция или строка пусты.

Параметры: Нет.

import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.string.shouldBeEmpty

val emptyList = listOf<Int>()
emptyList.shouldBeEmpty()

val emptyString = ""
emptyString.shouldBeEmpty()

7. shouldStartWith / shouldEndWith — проверка начала и конца строки

Что делает: Проверяет, что строка начинается или заканчивается указанной подстрокой.

Параметры: Подстрока (String).

import io.kotest.matchers.string.shouldStartWith
import io.kotest.matchers.string.shouldEndWith

val url = "https://pythonlib.ru"
url shouldStartWith "https"
url shouldEndWith ".ru"

8. shouldMatch — проверка по регулярному выражению

Что делает: Проверяет, что строка соответствует регулярному выражению.

Параметры: Регулярное выражение (String или Regex).

import io.kotest.matchers.string.shouldMatch

val email = "user@example.com"
email shouldMatch Regex("^[A-Za-z0-9+_.-]+@(.+)$") // Тест пройден

9. shouldBeGreaterThan / shouldBeLessThan — числовые сравнения

Что делает: Проверяет числовые отношения "больше" и "меньше".

Параметры: Число для сравнения.

import io.kotest.matchers.numbers.shouldBeGreaterThan
import io.kotest.matchers.numbers.shouldBeLessThan

val x = 10
x shouldBeGreaterThan 5
x shouldBeLessThan 20

10. forAll — property-based testing (проверка свойств)

Что делает: Генерирует множество случайных значений и проверяет, что для всех них выполняется указанное условие. Это мощный инструмент для нахождения граничных случаев.

Параметры: Генераторы (Arb) и лямбда с проверяемым условием.

import io.kotest.property.forAll
import io.kotest.property.arbitrary.int

// Проверяем, что для любого целого числа выполняется: x + 1 > x
forAll<Int> { x ->
    (x + 1) > x
}

// С двумя параметрами
forAll<Int, Int> { a, b ->
    a + b == b + a // Коммутативность сложения
}

Реальный практический пример использования

Рассмотрим более сложный пример: тестирование сервиса для работы с пользователями, который взаимодействует с базой данных (через репозиторий). Мы будем использовать моки (MockK) и проверять бизнес-логику.

// Допустим, у нас есть такие классы:
data class User(val id: Long, val name: String, val email: String)

interface UserRepository {
    fun findById(id: Long): User?
    fun save(user: User): User
}

class UserService(private val repository: UserRepository) {
    fun getUserName(id: Long): String {
        val user = repository.findById(id)
        return user?.name ?: throw NoSuchElementException("User not found")
    }

    fun createUser(name: String, email: String): User {
        require(name.isNotBlank()) { "Name cannot be blank" }
        require(email.contains("@")) { "Invalid email" }
        val newUser = User(id = 0, name = name, email = email)
        return repository.save(newUser)
    }
}

// Тест с использованием Kotest и MockK
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldThrow
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify

class UserServiceTest : FunSpec({
    val mockRepository = mockk<UserRepository>()
    val userService = UserService(mockRepository)

    context("getUserName") {
        test("должен вернуть имя, если пользователь существует") {
            // Настраиваем мок
            every { mockRepository.findById(1) } returns User(1, "Alice", "alice@test.com")

            val name = userService.getUserName(1)
            name shouldBe "Alice"
            verify { mockRepository.findById(1) } // Проверяем, что метод был вызван
        }

        test("должен выбросить исключение, если пользователь не найден") {
            every { mockRepository.findById(999) } returns null

            shouldThrow<NoSuchElementException> {
                userService.getUserName(999)
            }
        }
    }

    context("createUser") {
        test("должен создать пользователя с валидными данными") {
            val newUser = User(0, "Bob", "bob@test.com")
            every { mockRepository.save(any()) } returns newUser.copy(id = 1)

            val createdUser = userService.createUser("Bob", "bob@test.com")
            createdUser.id shouldBe 1
            createdUser.name shouldBe "Bob"
        }

        test("должен выбросить исключение при пустом имени") {
            shouldThrow<IllegalArgumentException> {
                userService.createUser("", "test@test.com")
            }
        }
    }                    

Рекомендаций