Тестирование Kotlin: с чего начать начинающему — полный гайд

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

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

Начать курс

Введение: Почему тестирование Kotlin — это суперсила, а не скучная обязанность

Представьте: вы написали приложение на Kotlin. Оно работает. Вы счастливы. Но проходит неделя, вы добавляете новую фичу — и вдруг ломается старая. Знакомо? Если нет — вы либо гений, либо никогда не писали код без тестов. Тестирование Kotlin — это не просто галочка в чек-листе разработчика. Это ваш личный «щит» от багов, способ спать спокойно после деплоя и инструмент, который делает код чище и понятнее.

В этой статье я покажу, с чего начать тестирование на Kotlin начинающему разработчику. Мы разберем реальные примеры, популярные библиотеки и лайфхаки, которые сэкономят вам часы дебага. Вы узнаете, как писать юнит-тесты, мокать зависимости и тестировать корутины. Поехали!

Шаг 1: Настройка окружения — первые 5 минут

Прежде чем писать первый тест, нужно подключить зависимости. Если вы используете Gradle (а вы, скорее всего, используете), добавьте в build.gradle.kts следующие строки:

// build.gradle.kts
dependencies {
    // JUnit 5 — основа юнит-тестирования
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    
    // MockK — лучшая библиотека для моков в Kotlin
    testImplementation("io.mockk:mockk:1.13.8")
    
    // AssertJ — красивые и читаемые assertion'ы
    testImplementation("org.assertj:assertj-core:3.24.2")
    
    // Для тестирования корутин
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}

tasks.test {
    useJUnitPlatform()
}

Готово! Теперь у вас есть всё необходимое для написания тестов. Обратите внимание: мы используем JUnit 5 (а не 4) — это современный стандарт, который отлично дружит с Kotlin. MockK выбран не случайно: он понимает null-safety, extension-функции и другие фишки Kotlin, с которыми Mockito справляется плохо.

Шаг 2: Ваш первый юнит-тест на Kotlin

Давайте напишем простой класс и протестируем его. Допустим, у нас есть калькулятор скидок:

// DiscountCalculator.kt
class DiscountCalculator {
    fun calculateDiscount(price: Double, discountPercent: Int): Double {
        require(price >= 0) { "Цена не может быть отрицательной" }
        require(discountPercent in 0..100) { "Скидка должна быть от 0 до 100" }
        return price * (100 - discountPercent) / 100.0
    }
}

Теперь напишем тест. Создайте файл DiscountCalculatorTest.kt в папке src/test/kotlin:

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.assertj.core.api.Assertions.assertThat

class DiscountCalculatorTest {
    
    private val calculator = DiscountCalculator()
    
    @Test
    fun `должен правильно считать скидку 20%`() {
        val result = calculator.calculateDiscount(1000.0, 20)
        assertThat(result).isEqualTo(800.0)
    }
    
    @Test
    fun `должен выбросить исключение при отрицательной цене`() {
        assertThrows<IllegalArgumentException> {
            calculator.calculateDiscount(-100.0, 10)
        }
    }
    
    @Test
    fun `должен вернуть полную цену при скидке 0%`() {
        val result = calculator.calculateDiscount(500.0, 0)
        assertThat(result).isEqualTo(500.0)
    }
}

Обратите внимание на стиль: мы используем обратные кавычки для названий тестов на русском (Kotlin это позволяет). Это делает тесты читаемыми даже для нетехнического коллеги. AssertJ даёт fluent-интерфейс: assertThat(result).isEqualTo(800.0) — читается как английское предложение.

Шаг 3: Моки с MockK — когда тестируем зависимости

В реальных приложениях классы редко живут сами по себе. Они обращаются к базе данных, API или другим сервисам. Чтобы не зависеть от внешних систем, мы используем моки. Допустим, у нас есть сервис отправки email:

// EmailService.kt
interface EmailService {
    fun send(to: String, subject: String, body: String)
}

// NotificationService.kt
class NotificationService(private val emailService: EmailService) {
    fun notifyUser(userEmail: String, message: String) {
        emailService.send(userEmail, "Уведомление", message)
    }
}

Теперь напишем тест с моком:

import io.mockk.*
import org.junit.jupiter.api.Test

class NotificationServiceTest {
    
    @Test
    fun `должен отправить email при уведомлении пользователя`() {
        // Создаём мок
        val mockEmailService = mockk<EmailService>()
        
        // Описываем ожидаемое поведение: когда вызываем send — ничего не делаем
        every { mockEmailService.send(any(), any(), any()) } returns Unit
        
        // Создаём тестируемый объект
        val service = NotificationService(mockEmailService)
        
        // Вызываем метод
        service.notifyUser("test@example.com", "Привет!")
        
        // Проверяем, что метод send был вызван ровно один раз с правильными аргументами
        verify(exactly = 1) { 
            mockEmailService.send("test@example.com", "Уведомление", "Привет!") 
        }
    }
}

Важно: MockK использует every { ... } returns ... для настройки поведения и verify { ... } для проверки вызовов. Это гораздо удобнее, чем статические методы Mockito. Если вы переходите с Java, привыкнете за 10 минут.

Шаг 4: Тестирование корутин — не бойтесь async

Kotlin-разработчики обожают корутины, но тестировать их поначалу страшно. На самом деле всё просто с библиотекой kotlinx-coroutines-test. Представьте, что у нас есть репозиторий, который загружает данные:

class UserRepository(private val api: UserApi) {
    suspend fun getUser(id: Int): User {
        return api.fetchUser(id)
    }
}

Тест с корутинами:

import kotlinx.coroutines.test.runTest
import io.mockk.*
import org.junit.jupiter.api.Test

class UserRepositoryTest {
    
    @Test
    fun `должен вернуть пользователя по id`() = runTest {
        // runTest — магия, которая делает тесты корутин синхронными
        val mockApi = mockk<UserApi>()
        val expectedUser = User(1, "Alice")
        
        coEvery { mockApi.fetchUser(1) } returns expectedUser
        
        val repository = UserRepository(mockApi)
        val user = repository.getUser(1)
        
        assertThat(user).isEqualTo(expectedUser)
    }
}

Обратите внимание на coEvery вместо every — это специальная функция для suspend-функций. А runTest автоматически управляет виртуальным временем, так что тесты выполняются мгновенно, без реальных задержек.

Шаг 5: Параметризованные тесты — меньше

Блоги

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