Введение: Почему тестирование 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 автоматически управляет виртуальным временем, так что тесты выполняются мгновенно, без реальных задержек.