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

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

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

Начать курс

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

Mockk — это современная библиотека для мокирования (создания имитаций объектов) в языке программирования Kotlin. Она специально разработана с учетом особенностей Kotlin: нуллабильности, функций-расширений, корутин, объектов-компаньонов и других возможностей, которые отсутствуют в Java.

Зачем нужна библиотека Mockk? При написании модульных тестов мы часто сталкиваемся с ситуацией, когда тестируемый класс зависит от других классов, сервисов, баз данных или внешних API. Чтобы изолировать тестируемый код и сделать тесты быстрыми, надежными и независимыми от внешних факторов, мы создаем "моки" (mock objects) — объекты-заглушки, которые имитируют поведение реальных зависимостей.

Основные возможности Mockk:

  • Создание моков для классов, интерфейсов и объектов
  • Поддержка аннотаций (@MockK, @RelaxedMockK, @SpyK)
  • Работа с корутинами (coEvery, coVerify)
  • Мокирование функций-расширений
  • Мокирование статических методов и объектов-компаньонов
  • Поддержка слотов (slot) и захвата аргументов
  • Поддержка частичного мокирования (spy)

Установка Mockk

Mockk распространяется через Maven Central. Для подключения к проекту добавьте зависимость в файл build.gradle.kts (для Gradle с Kotlin DSL):

// build.gradle.kts
dependencies {
    testImplementation("io.mockk:mockk:1.13.10")
}

Для Gradle с Groovy DSL:

// build.gradle
dependencies {
    testImplementation "io.mockk:mockk:1.13.10"
}

Для Maven:

<dependency>
    <groupId>io.mockk</groupId>
    <artifactId>mockk</artifactId>
    <version>1.13.10</version>
    <scope>test</scope>
</dependency>

Если вы используете корутины, дополнительно подключать ничего не нужно — поддержка корутин встроена в Mockk.

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

Рассмотрим простейший пример. Допустим, у нас есть сервис UserService, который зависит от репозитория UserRepository:

// Классы для тестирования
class UserRepository {
    fun findById(id: Long): String {
        // Реальная реализация обращается к БД
        return "Real User"
    }
}

class UserService(private val repository: UserRepository) {
    fun getUserName(id: Long): String {
        val user = repository.findById(id)
        return if (user.isNotEmpty()) user else "Unknown"
    }
}

Теперь напишем тест с использованием Mockk:

import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Test

class UserServiceTest {

    @Test
    fun `getUserName should return user name from repository`() {
        // 1. Создаем мок для репозитория
        val mockRepository = mockk<UserRepository>()
        
        // 2. Определяем поведение мока
        every { mockRepository.findById(1L) } returns "John Doe"
        
        // 3. Создаем тестируемый объект с моком
        val service = UserService(mockRepository)
        
        // 4. Вызываем тестируемый метод
        val result = service.getUserName(1L)
        
        // 5. Проверяем результат
        assert(result == "John Doe")
        
        // 6. Проверяем, что метод был вызван ровно 1 раз
        verify(exactly = 1) { mockRepository.findById(1L) }
    }
}

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

Рассмотрим 10 ключевых методов библиотеки с подробным описанием и примерами.

1. mockk() — создание мок-объекта

Что делает: Создает мок-объект для указанного класса или интерфейса. Все методы мока по умолчанию возвращают значения по умолчанию (null, 0, false) и выбрасывают исключение, если поведение не задано.

Параметры:

  • cls: KClass<T> — класс для мокирования (опционально, если тип выводится)
  • relaxed: Boolean = false — если true, все методы возвращают "пустые" значения без необходимости задавать every
  • name: String? = null — имя мока для отладки

Пример:

// Простой мок
val repository = mockk<UserRepository>()

// Расслабленный мок (relaxed) — все методы возвращают значения по умолчанию
val relaxedRepo = mockk<UserRepository>(relaxed = true)

// Мок с именем
val namedMock = mockk<UserRepository>(name = "userRepo")

2. every — определение поведения мока

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

Параметры: Не принимает параметров напрямую, но внутри блока every используется синтаксис вызова метода с аргументами.

Пример:

val mock = mockk<Calculator>()

// Возврат конкретного значения
every { mock.add(2, 3) } returns 5

// Возврат значения на основе аргументов
every { mock.add(any(), any()) } answers { firstArg<Int>() + secondArg<Int>() }

// Выброс исключения
every { mock.divide(1, 0) } throws ArithmeticException("Division by zero")

// Множественные ответы (первый вызов — 1, второй — 2, третий и далее — 3)
every { mock.getCount() } returnsMany listOf(1, 2, 3)

3. verify — проверка вызовов

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

Параметры:

  • exactly: Int? = null — точное количество вызовов
  • atLeast: Int = 0 — минимум вызовов
  • atMost: Int = Int.MAX_VALUE — максимум вызовов
  • timeout: Long = 0 — таймаут для асинхронных проверок
  • inverse: Boolean = false — инвертировать проверку (проверить, что НЕ был вызван)

Пример:

val mock = mockk<UserRepository>()
every { mock.findById(1L) } returns "User"

// Вызываем метод
mock.findById(1L)
mock.findById(1L)

// Проверка: вызван ровно 2 раза
verify(exactly = 2) { mock.findById(1L) }

// Проверка: вызван хотя бы 1 раз
verify(atLeast = 1) { mock.findById(1L) }

// Проверка: НЕ был вызван с другим аргументом
verify(inverse = true) { mock.findById(2L) }

// Проверка порядка вызовов
verifyOrder {
    mock.findById(1L)
    mock.findById(1L)
}

4. slot — захват аргументов

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

Параметры: Создается через slot<T>(), где T — тип захватываемого аргумента.

Пример:

data class User(val name: String, val age: Int)

class UserService(private val repo: UserRepository) {
    fun saveUser(user: User) {
        repo.save(user)
    }
}

@Test
fun `should capture user argument`() {
    val mockRepo = mockk<UserRepository>()
    val userSlot = slot<User>()
    
    every { mockRepo.save(capture(userSlot)) } returns Unit
    
    val service = UserService(mockRepo)
    service.saveUser(User("Alice", 30))
    
    // Проверяем захваченный аргумент
    assert(userSlot.captured.name == "Alice")
    assert(userSlot.captured.age == 30)
}

5. spy — частичное мокирование

Что делает: Создает "шпиона" (spy) — объект, который использует реальную реализацию, но позволяет переопределить поведение отдельных методов. Полезно, когда нужно протестировать только часть класса, оставив остальную логику нетронутой.

Параметры:

  • obj: T — реальный объект, на основе которого создается шпион
  • recordPrivateCalls: Boolean = false — записывать вызовы приватных методов

Пример:

class Calculator {
    fun add(a: Int, b: Int) = a + b
    fun multiply(a: Int, b: Int) = a * b
}

@Test
fun `spy example`() {
    val realCalc = Calculator()
    val spyCalc = spyk(realCalc)
    
    // Переопределяем только метод multiply
    every { spyCalc.multiply(any(), any()) } returns 100
    
    // add() использует реальную реализацию
    assert(spyCalc.add(2, 3) == 5)
    
    // multiply() использует мокированную версию
    assert(spyCalc.multiply(2, 3) == 100)
}

6. mockkObject — мокирование объекта-синглтона

Что делает: Позволяет мокировать Kotlin-объекты (object declarations), которые являются синглтонами. Это необходимо, так как обычный mockk() не работает с объектами.

Параметры: Принимает ссылку на объект.

Пример:

object DatabaseConfig {
    fun getConnectionString(): String = "jdbc:mysql://localhost:3306/db"
}

class DatabaseService {
    fun connect() {
        val connStr = DatabaseConfig.getConnectionString()
        // ... логика подключения
    }
}

@Test
fun `mock object example`() {
    mockkObject(DatabaseConfig)
    every { DatabaseConfig.getConnectionString() } returns "mock_connection_string"
    
    val service = DatabaseService()
    service.connect()
    
    verify { DatabaseConfig.getConnectionString() }
    
    // Важно: после теста нужно очистить мок
    unmockkObject(DatabaseConfig)
}

7. coEvery и coVerify — работа с корутинами

Что делает: Аналоги every и verify, но для suspend-функций (функций, работающих с корутинами). Позволяет мокировать и проверять асинхронные вызовы.

Параметры: Те же, что и у every/verify.

Пример:

class AsyncRepository {
    suspend fun fetchData(): String {
        delay(1000) // имитация долгой операции
        return "Real Data"
    }
}

class AsyncService(private val repo: AsyncRepository) {
    suspend fun getData(): String {
        return repo.fetchData()
    }
}

@Test
fun `coroutine mock example`() = runBlocking {
    val mockRepo = mockk<AsyncRepository>()
    coEvery { mockRepo.fetchData() } returns "Mock Data"
    
    val service = AsyncService(mockRepo)
    val result = service.getData()
    
    assert(result == "Mock Data")
    coVerify { mockRepo.fetchData() }
}

8. every { ... } answers — динамические ответы

Что делает: Позволяет задать поведение мока на основе переданных аргументов. Внутри блока answers доступны функции для получения аргументов: firstArg(), secondArg(), arg(n) и другие.

Параметры:

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