Моки и фикстуры в Kotlin: гайд по тестированию без боли

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

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

Начать курс

Введение: Почему тесты без моков — как кофе без кофеина?

Привет, коллега! Если ты пишешь на Kotlin и хочешь, чтобы твой код не ломался при каждом втором коммите, рано или поздно ты придёшь к тестам. А в тестах — к mock-объектам и фикстурам. Без них юнит-тесты превращаются в интеграционные: ты тестируешь не один класс, а целую цепочку зависимостей. И если база данных упадёт или API вернёт 500 — тест упадёт, хотя твой код может быть идеален.

В этой статье мы разберём, как правильно использовать Mock-объекты и фикстуры в Kotlin тестах, чтобы тесты были быстрыми, надёжными и говорили тебе правду, а не «всё сломалось, чини». Поехали!

Что такое mock и зачем он нужен в Kotlin?

Mock-объект — это имитация реального объекта. Вместо того чтобы поднимать целый сервер или базу данных, мы создаём «заглушку», которая ведёт себя так, как мы скажем. Это позволяет изолировать тестируемый код от внешнего мира.

Основные понятия

  • Mock — объект, у которого мы контролируем поведение методов. Мы можем задать, что он вернёт, и проверить, вызывался ли он.
  • Stub — частный случай мока, который только возвращает заранее заданные значения. Не проверяет вызовы.
  • Fixture (фикстура) — набор предварительно подготовленных данных или состояний, необходимых для выполнения теста. Например, тестовый пользователь или настройки БД.

В мире Kotlin есть два главных игрока: MockK (родной для Kotlin) и Mockito (популярный в Java). Мы будем использовать MockK — он лучше дружит с корутинами, функциями-расширениями и sealed-классами.

Настройка проекта: добавляем MockK и JUnit 5

Для начала добавим зависимости в build.gradle.kts:

dependencies {
    testImplementation("io.mockk:mockk:1.13.8")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}

Теперь можно писать тесты. Создадим простой сервис и протестируем его с помощью моков.

Первый пример: мокаем репозиторий

Представь, что у нас есть UserService, который зависит от UserRepository:

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

Теперь напишем тест, используя mock-объект для репозитория:

import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class UserServiceTest {

    private val repository = mockk<UserRepository>()
    private val service = UserService(repository)

    @Test
    fun `should return user name when user exists`() {
        // Arrange
        every { repository.findById(1L) } returns User(1L, "Alice")

        // Act
        val name = service.getUserName(1L)

        // Assert
        assertEquals("Alice", name)
        verify { repository.findById(1L) }
    }

    @Test
    fun `should throw exception when user not found`() {
        // Arrange
        every { repository.findById(any()) } returns null

        // Act & Assert
        assertFailsWith<IllegalArgumentException> {
            service.getUserName(99L)
        }
    }
}

Обрати внимание: every задаёт поведение мока, verify проверяет, что метод был вызван. Это основа mock-объектов и фикстур в Kotlin тестах.

Фикстуры: готовим данные заранее

Когда тестов много, дублировать создание данных — плохая практика. На помощь приходят фикстуры. В Kotlin их удобно реализовать через фабричные функции или объекты-компаньоны.

Пример фикстуры для User

object UserFixtures {
    fun alice() = User(1L, "Alice", "alice@example.com")
    fun bob() = User(2L, "Bob", "bob@example.com")
    fun defaultUser() = User(0L, "Default", "default@test.com")
}

Теперь в тесте можно писать:

every { repository.findById(1L) } returns UserFixtures.alice()

Это делает код чище и снижает вероятность опечаток. Фикстуры можно расширять: добавлять списки, вложенные объекты, разные сценарии.

MockK для корутин: suspend-функции

Kotlin славится корутинами, и MockK отлично их поддерживает. Для мока suspend-функций используем coEvery и coVerify:

class OrderService(private val paymentApi: PaymentApi) {
    suspend fun processOrder(orderId: String): Boolean {
        return paymentApi.charge(orderId)
    }
}

interface PaymentApi {
    suspend fun charge(orderId: String): Boolean
}

Тест:

@Test
fun `should process order successfully`() = runTest {
    // Arrange
    val api = mockk<PaymentApi>()
    val service = OrderService(api)

    coEvery { api.charge("order-1") } returns true

    // Act
    val result = service.processOrder("order-1")

    // Assert
    assertTrue(result)
    coVerify { api.charge("order-1") }
}

Обрати внимание на runTest — это функция из kotlinx-coroutines-test, которая нужна для запуска корутин в тестах. Без неё suspend-функции не вызвать.

Продвинутые техники: capture, slot и relaxed mock

Иногда нужно не просто проверить, что метод вызван, а ещё и посмотреть, с какими аргументами. Для этого используем slot:

@Test
fun `should save user with correct email`() {
    val repository = mockk<UserRepository>()
    val service = UserService(repository)
    val slot = slot<User>()

    every { repository.save(capture(slot)) } returns Unit

    service.registerUser("Charlie", "charlie@test.com")

    assertEquals("charlie@test.com", slot.captured.email)
}

Ещё одна полезная штука — relaxed mock. Это мок, который по умолчанию возвращает «пустые» значения для всех методов (0, null, false и т.д.). Удобно, если нужно замокать только часть поведения:

val repository = mockk<UserRepository>(relaxed = true)

Но будь осторожен: relaxed mock может скрыть ошибки, если ты забудешь задать нужное поведение.

Организация тестов: структура и best practices

Чтобы тесты не превратились в ка

Блоги

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