Mockito Kotlin: тестирование и создание моков для начинающих

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


Зачем вообще нужны моки

Представьте, что вы тестируете робот-пылесос. Чтобы проверить, включает ли он двигатель при старте, вам не нужна настоящая квартира с коврами — достаточно убедиться, что на плату двигателя пошёл сигнал. В программировании то же самое: когда вы тестируете ViewModel, которая дёргает сервер через Retrofit, вы не хотите реального интернет-соединения. Вы подменяете Retrofit заглушкой и говорите: «когда вызовут метод getUserName(), верни строку "Иван"». Это и есть мок — объект-пустышка, которым вы управляете. Через mockito kotlin такое поведение настраивается в несколько строк.


Добавление Mockito в проект

Для чистого Kotlin-проекта рекомендую сразу использовать обёртку mockito-kotlin. Она убирает все неудобства, связанные с null и дженериками из Java-версии Mockito, и даёт идиоматичный Kotlin-синтаксис. Вот что нужно добавить в build.gradle.kts:

dependencies {
    testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
    testImplementation("junit:junit:4.13.2")
}

Версии лучше уточнить на Maven Central. Вместе с mockito-kotlin подтянется и сам Mockito, поэтому отдельно его подключать не нужно. Для Android-проектов дополнительно потребуется библиотека mockito-android, если вы используете инструментальные тесты, но для юнит-тестов описанного набора достаточно.


Создание первого мока

Допустим, у нас есть простой репозиторий, который загружает профиль пользователя. Мы хотим протестировать логику, которая показывает приветственное сообщение, не дёргая реальный сервер. Вот как создать мок с помощью mockito kotlin:

interface UserRepository {
    fun getUserName(): String
}

val repository = mock<UserRepository>()

Одно слово mock — и у нас в руках объект, который притворяется UserRepository. Теперь можно настроить его поведение. Функция whenever принимает вызов метода и говорит, что вернуть:

whenever(repository.getUserName()).thenReturn("Анна")

Теперь любой вызов repository.getUserName() будет возвращать строку "Анна", будто сервер её прислал.


Проверка вызовов: verify

Создать заглушку мало. Часто нужно убедиться, что определённый метод действительно вызывался, и ровно столько раз, сколько ожидается. За это отвечает verify.

// вызываем метод дважды
repository.getUserName()
repository.getUserName()

// проверяем, что метод был вызван ровно 2 раза
verify(repository, times(2)).getUserName()

Если метод не вызывался или вызывался меньше/больше раз, тест упадёт с понятным сообщением. Можно не указывать times(2) — тогда verify проверит, что метод вызван ровно один раз.


Захват аргументов: ArgumentCaptor

Бывает, что метод принимает параметры, и нужно проверить, с какими именно аргументами он вызывался. Например, вы хотите убедиться, что при сохранении пользователя в базу был передан правильный email. Для этого есть argumentCaptor.

interface MailService {
    fun send(to: String, message: String)
}

val mailService = mock<MailService>()
// вызываем метод с аргументом
mailService.send("anna@example.com", "Привет")

// захватываем аргумент
val captor = argumentCaptor<String>()
verify(mailService).send(captor.capture(), any())

// проверяем захваченное значение
assertEquals("anna@example.com", captor.firstValue)

any() означает «любой аргумент этого типа». Он удобен, когда проверять нужно только часть параметров. Захваченные аргументы доступны через captor.firstValue, captor.secondValue или сразу списком через captor.allValues.


Тестируем ViewModel с моками

Теперь применим знания на практике. Типичный сценарий: ViewModel вызывает репозиторий, получает данные и отдаёт их во Flow для UI. Мы протестируем, что при успешном получении данных состояние меняется на Success.

class ProfileViewModel(private val repository: UserRepository) : ViewModel() {
    val greeting = MutableStateFlow<String>("")

    fun load() {
        greeting.value = "Привет, ${repository.getUserName()}!"
    }
}

Тест будет выглядеть так:

class ProfileViewModelTest {
    private val repository = mock<UserRepository>()
    private val viewModel = ProfileViewModel(repository)

    @Test
    fun `load should update greeting`() {
        whenever(repository.getUserName()).thenReturn("Иван")

        viewModel.load()

        assertEquals("Привет, Иван!", viewModel.greeting.value)
        verify(repository).getUserName()
    }
}

Тест изолирован: ни сети, ни базы данных. mockito kotlin позволяет написать такой тест за пару минут.


Заглушки для suspend-функций

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

interface Api {
    suspend fun fetchData(): String
}

val api = mock<Api>()
val scope = TestScope()

scope.launch {
    whenever(api.fetchData()).thenReturn("ответ")
    val result = api.fetchData()
    println(result) // ответ
}

Ключевой момент: тест нужно запускать внутри runTest { ... } из библиотеки kotlinx-coroutines-test. Это создаст тестовое окружение, в котором suspend-функции выполняются последовательно и предсказуемо.


Основные аннотации Mockito

В сложных тестовых классах для сокращения кода используют аннотации. Вот основные из арсенала mockito kotlin:

Аннотация Что делает Пример
@Mock Создаёт мок-объект lateinit var repo: UserRepository
@InjectMocks Внедряет моки в тестируемый объект lateinit var viewModel: ProfileViewModel
@MockK (MockK) Альтернатива от библиотеки MockK Используется с mockk<T>()
@Rule Правило для JUnit (замена runner) Подключает MockitoRule

Для Kotlin аннотации работают так же, как в Java, но чаще предпочитают программный подход (mock(), whenever) — он лаконичнее и лучше читается в цепочках вызовов.


Советы начинающим

  • Тестируйте только свой код. Не проверяйте, что Retrofit действительно шлёт HTTP-запросы — мокайте его и проверяйте, что ваш код правильно обрабатывает ответ.
  • Один тест — одна проверка. Если тест проверяет и успешный сценарий, и ошибку сети, его сложно читать и он часто ломается. Разделяйте сценарии.
  • Избегайте any() без необходимости. Если можете указать конкретное значение — укажите. Это делает тест строже и защищает от ложных срабатываний.
  • Пишите тесты одновременно с кодом. Отложенные тесты почти никогда не пишутся. Взялись за новый класс — сразу создайте тестовый класс.
  • Не забывайте про runTest. Без тестового корутин-скоупа suspend-функции в моках будут работать неправильно или падать.

Коротко о главном

Mockito Kotlin — это must-have инструмент для юнит-тестирования на Kotlin. Вы научились создавать моки через mock(), настраивать их поведение через whenever, проверять вызовы через verify и захватывать аргументы через argumentCaptor. С этими знаниями можно протестировать ViewModel, репозиторий или любой другой класс, изолировав его от реальных зависимостей. Начните с небольшого: напишите тест для одного метода вашего текущего проекта — и вы почувствуете, насколько спокойнее становится работа, когда код покрыт тестами.