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