781 lines
37 KiB
Markdown
781 lines
37 KiB
Markdown
---
|
||
title: MockK
|
||
description: Моки, стабы, spyk, проверка вызовов, мокирование object, companion, функций верхнего уровня и suspend
|
||
tags:
|
||
- kotlin
|
||
- testing
|
||
- mockk
|
||
---
|
||
|
||
MockK - библиотека моков, написанная для Kotlin. Главное её отличие от Mockito в том, что финальные классы, `object`, функции верхнего уровня и `suspend`-функции мокаются без дополнительной настройки.
|
||
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.mockk
|
||
import io.mockk.verify
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
data class User(val id: Int, val name: String)
|
||
|
||
interface UserRepo {
|
||
fun findById(id: Int): User?
|
||
}
|
||
|
||
class FirstMockTest {
|
||
@Test
|
||
fun `мок возвращает заданное значение`() {
|
||
val repo = mockk<UserRepo>()
|
||
every { repo.findById(1) } returns User(1, "Иван")
|
||
|
||
assertEquals("Иван", repo.findById(1)?.name)
|
||
verify { repo.findById(1) }
|
||
}
|
||
}
|
||
```
|
||
Три шага: `mockk<T>()` создаёт заглушку, `every { } returns` задаёт поведение, `verify { }` проверяет вызов.
|
||
|
||
## Подключение
|
||
```kotlin
|
||
// build.gradle.kts
|
||
dependencies {
|
||
testImplementation("io.mockk:mockk:1.14.6")
|
||
}
|
||
|
||
tasks.test { useJUnitPlatform() }
|
||
```
|
||
| Артефакт | Зачем |
|
||
|---|---|
|
||
| `io.mockk:mockk` | всё для JVM-тестов, включая JUnit 5 extension |
|
||
| `io.mockk:mockk-android` | инструментальные тесты Android |
|
||
| `io.mockk:mockk-agent-jvm` | подтягивается транзитивно, отдельно не нужен |
|
||
|
||
Зависимость одна - `mockk` тянет `byte-buddy` и `objenesis` сам.
|
||
|
||
## Создание моков
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.mockk
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
interface Repo {
|
||
fun count(): Int
|
||
fun name(): String
|
||
fun flag(): Boolean
|
||
fun items(): List<String>
|
||
fun clear()
|
||
}
|
||
|
||
class MockModesTest {
|
||
@Test
|
||
fun `строгий мок требует стаба на каждый вызов`() {
|
||
val repo = mockk<Repo>()
|
||
every { repo.count() } returns 7
|
||
assertEquals(7, repo.count())
|
||
// repo.name() бросил бы MockKException
|
||
}
|
||
|
||
@Test
|
||
fun `relaxed возвращает значения по умолчанию`() {
|
||
val repo = mockk<Repo>(relaxed = true)
|
||
assertEquals(0, repo.count())
|
||
assertEquals("", repo.name())
|
||
assertEquals(false, repo.flag())
|
||
assertEquals(emptyList(), repo.items())
|
||
}
|
||
|
||
@Test
|
||
fun `relaxUnitFun ослабляет только Unit-методы`() {
|
||
val repo = mockk<Repo>(relaxUnitFun = true)
|
||
repo.clear() // ок
|
||
// repo.count() всё ещё бросит MockKException
|
||
}
|
||
|
||
@Test
|
||
fun `имя мока попадает в сообщение об ошибке`() {
|
||
val repo = mockk<Repo>(name = "репозиторий")
|
||
val message = runCatching { repo.count() }.exceptionOrNull()?.message
|
||
assertEquals(true, message?.contains("репозиторий"))
|
||
}
|
||
}
|
||
```
|
||
| Форма | Поведение на нестабованном вызове |
|
||
|---|---|
|
||
| `mockk<T>()` | `MockKException: no answer found` |
|
||
| `mockk<T>(relaxed = true)` | значение по умолчанию: `0`, `""`, `false`, пустая коллекция, мок для ссылочного типа |
|
||
| `mockk<T>(relaxUnitFun = true)` | `Unit`-методы проходят, остальные бросают |
|
||
| `mockk<T>(name = "x")` | строгий, но с понятным именем в ошибке |
|
||
|
||
`relaxed = true` для ссылочных типов возвращает не `null`, а дочерний мок - `repo.findById(1)` даст `User(child of #1#2)`. Это удобно для цепочек, но скрывает забытые стабы.
|
||
|
||
## Задание поведения
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.just
|
||
import io.mockk.Runs
|
||
import io.mockk.mockk
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
data class User(val id: Int, val name: String)
|
||
|
||
interface UserRepo {
|
||
fun findById(id: Int): User?
|
||
fun count(): Int
|
||
fun deleteAll()
|
||
}
|
||
|
||
class AnswersTest {
|
||
@Test
|
||
fun `returns и returnsMany`() {
|
||
val repo = mockk<UserRepo>()
|
||
every { repo.count() } returnsMany listOf(1, 2, 3)
|
||
// последнее значение повторяется для всех дальнейших вызовов
|
||
assertEquals(listOf(1, 2, 3, 3), List(4) { repo.count() })
|
||
}
|
||
|
||
@Test
|
||
fun `answers видит аргументы вызова`() {
|
||
val repo = mockk<UserRepo>()
|
||
every { repo.findById(any()) } answers {
|
||
User(firstArg(), "пользователь ${firstArg<Int>()}")
|
||
}
|
||
assertEquals("пользователь 7", repo.findById(7)?.name)
|
||
}
|
||
|
||
@Test
|
||
fun `throws`() {
|
||
val repo = mockk<UserRepo>()
|
||
every { repo.count() } throws IllegalStateException("нет соединения")
|
||
val e = runCatching { repo.count() }.exceptionOrNull()
|
||
assertEquals("нет соединения", e?.message)
|
||
}
|
||
|
||
@Test
|
||
fun `just Runs для Unit`() {
|
||
val repo = mockk<UserRepo>()
|
||
every { repo.deleteAll() } just Runs
|
||
repo.deleteAll()
|
||
}
|
||
}
|
||
```
|
||
| Инфикс | Что делает |
|
||
|---|---|
|
||
| `returns x` | всегда одно значение |
|
||
| `returnsMany listOf(...)` | значения по порядку, последнее повторяется |
|
||
| `answers { }` | лямбда с доступом к `firstArg()`, `secondArg()`, `arg<T>(n)`, `call` |
|
||
| `answersMany listOf(...)` | список ответов |
|
||
| `throws e` | бросает исключение |
|
||
| `throwsMany listOf(...)` | исключения по порядку |
|
||
| `just Runs` | для `Unit`-функций |
|
||
| `propertyType` / `nullablePropertyType` | уточнение типа для дженериков |
|
||
|
||
`returnsMany` после исчерпания списка **повторяет последний элемент**, а не бросает - из четырёх вызовов `[1, 2, 3]` даст `1 2 3 3`.
|
||
|
||
## Сопоставители аргументов
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.mockk
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
import kotlin.test.assertNull
|
||
|
||
data class User(val id: Int, val name: String)
|
||
|
||
interface UserRepo {
|
||
fun findById(id: Int): User?
|
||
}
|
||
|
||
class MatchersTest {
|
||
@Test
|
||
fun `сопоставители по условию`() {
|
||
val repo = mockk<UserRepo>()
|
||
every { repo.findById(more(10)) } returns User(0, "большой")
|
||
every { repo.findById(range(1, 9)) } returns User(0, "малый")
|
||
every { repo.findById(eq(0)) } returns null
|
||
|
||
assertEquals("большой", repo.findById(50)?.name)
|
||
assertEquals("малый", repo.findById(5)?.name)
|
||
assertNull(repo.findById(0))
|
||
}
|
||
}
|
||
```
|
||
| Сопоставитель | Условие |
|
||
|---|---|
|
||
| `any()` | любой аргумент |
|
||
| `eq(x)` / `neq(x)` | равен / не равен по `equals` |
|
||
| `refEq(x)` | та же ссылка |
|
||
| `isNull()` / `isNull(inverse = true)` | `null` или нет |
|
||
| `ofType<T>()` | конкретный тип |
|
||
| `more(x)` / `less(x)` | больше / меньше, с `andEquals` для нестрогого |
|
||
| `range(a, b)` | в диапазоне |
|
||
| `match { it.name.isNotBlank() }` | произвольный предикат |
|
||
| `capture(slot)` | захват значения |
|
||
| `withArg { assert(...) }` | проверка внутри `verify` |
|
||
|
||
Важно: позже заданный стаб **перекрывает** ранее заданный для тех же аргументов. `every { repo.findById(1) } returns a` после `every { repo.findById(any()) } returns b` даёт `a` для `1`; в обратном порядке `any()` перехватит и `1`.
|
||
|
||
## Захват аргументов
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.mockk
|
||
import io.mockk.slot
|
||
import io.mockk.verify
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
import kotlin.test.assertTrue
|
||
|
||
data class User(val id: Int, val name: String)
|
||
|
||
interface UserRepo {
|
||
fun findById(id: Int): User?
|
||
fun save(user: User): User
|
||
}
|
||
|
||
class CaptureTest {
|
||
@Test
|
||
fun `slot хранит последний аргумент`() {
|
||
val repo = mockk<UserRepo>()
|
||
val saved = slot<User>()
|
||
every { repo.save(capture(saved)) } answers { saved.captured }
|
||
|
||
repo.save(User(5, "Анна"))
|
||
assertEquals("Анна", saved.captured.name)
|
||
}
|
||
|
||
@Test
|
||
fun `список собирает все вызовы`() {
|
||
val repo = mockk<UserRepo>(relaxed = true)
|
||
val ids = mutableListOf<Int>()
|
||
every { repo.findById(capture(ids)) } returns null
|
||
|
||
repo.findById(1)
|
||
repo.findById(2)
|
||
repo.findById(3)
|
||
assertEquals(listOf(1, 2, 3), ids)
|
||
}
|
||
|
||
@Test
|
||
fun `withArg проверяет прямо в verify`() {
|
||
val repo = mockk<UserRepo>(relaxed = true)
|
||
repo.save(User(9, "Олег"))
|
||
verify { repo.save(withArg { assertTrue(it.name.startsWith("О")) }) }
|
||
}
|
||
}
|
||
```
|
||
`slot<T>()` хранит одно значение - обращение к `captured` до вызова даёт `IllegalStateException`. Для всех вызовов нужен `MutableList`. `withArg` короче, когда захваченное значение нужно только для одной проверки.
|
||
|
||
## Проверка вызовов
|
||
```kotlin
|
||
import io.mockk.confirmVerified
|
||
import io.mockk.mockk
|
||
import io.mockk.verify
|
||
import io.mockk.verifyOrder
|
||
import io.mockk.verifySequence
|
||
import kotlin.test.Test
|
||
|
||
data class User(val id: Int, val name: String)
|
||
|
||
interface UserRepo {
|
||
fun findById(id: Int): User?
|
||
fun save(user: User): User
|
||
fun findAll(): List<User>
|
||
}
|
||
|
||
class VerifyTest {
|
||
@Test
|
||
fun `количество вызовов`() {
|
||
val repo = mockk<UserRepo>(relaxed = true)
|
||
repo.findById(1)
|
||
repo.findById(1)
|
||
|
||
verify(exactly = 2) { repo.findById(1) }
|
||
verify(atLeast = 1) { repo.findById(any()) }
|
||
verify(exactly = 0) { repo.findAll() }
|
||
}
|
||
|
||
@Test
|
||
fun `порядок и полная последовательность`() {
|
||
val repo = mockk<UserRepo>(relaxed = true)
|
||
repo.findById(1)
|
||
repo.save(User(1, "a"))
|
||
repo.findAll()
|
||
|
||
// verifyOrder допускает вызовы между проверяемыми
|
||
verifyOrder {
|
||
repo.findById(1)
|
||
repo.findAll()
|
||
}
|
||
// verifySequence требует ровно эти вызовы и только их
|
||
verifySequence {
|
||
repo.findById(1)
|
||
repo.save(any())
|
||
repo.findAll()
|
||
}
|
||
}
|
||
|
||
@Test
|
||
fun `confirmVerified ловит непроверенные вызовы`() {
|
||
val repo = mockk<UserRepo>(relaxed = true)
|
||
repo.findById(1)
|
||
repo.findAll()
|
||
|
||
verify { repo.findById(1) }
|
||
// findAll не проверен - confirmVerified бросит AssertionError
|
||
val e = runCatching { confirmVerified(repo) }.exceptionOrNull()
|
||
kotlin.test.assertTrue(e is AssertionError)
|
||
}
|
||
}
|
||
```
|
||
| Функция | Что требует |
|
||
|---|---|
|
||
| `verify { }` | хотя бы один подходящий вызов |
|
||
| `verify(exactly = n)` | ровно `n` вызовов |
|
||
| `verify(atLeast = n)`, `verify(atMost = n)` | границы |
|
||
| `verify(exactly = 0)` | вызова не было |
|
||
| `verifyAll { }` | все перечисленные вызовы и ничего больше, порядок свободный |
|
||
| `verifyOrder { }` | перечисленные в этом порядке, между ними допустимы другие |
|
||
| `verifySequence { }` | ровно эти вызовы, ровно в этом порядке |
|
||
| `confirmVerified(mock)` | все вызовы мока были проверены |
|
||
| `excludeRecords { }` | исключить вызовы из учёта `confirmVerified` |
|
||
| `checkUnnecessaryStub(mock)` | ни один стаб не остался неиспользованным |
|
||
|
||
`confirmVerified` и `checkUnnecessaryStub` бросают `AssertionError`, то есть выглядят как обычный провал утверждения, а не как ошибка библиотеки.
|
||
|
||
## spyk - частичный мок
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.mockk
|
||
import io.mockk.spyk
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
data class User(val id: Int, val name: String)
|
||
|
||
interface UserRepo {
|
||
fun findById(id: Int): User?
|
||
}
|
||
|
||
open class Service(private val repo: UserRepo) {
|
||
open fun greet(id: Int): String = "Привет, ${repo.findById(id)?.name ?: "гость"}"
|
||
open fun helper(): Int = 42
|
||
fun usesHelper(): Int = helper() * 2
|
||
}
|
||
|
||
class SpykTest {
|
||
@Test
|
||
fun `spyk выполняет настоящий код пока метод не застабован`() {
|
||
val repo = mockk<UserRepo>()
|
||
every { repo.findById(1) } returns User(1, "Иван")
|
||
val service = spyk(Service(repo))
|
||
|
||
assertEquals("Привет, Иван", service.greet(1))
|
||
|
||
every { service.helper() } returns 10
|
||
assertEquals(20, service.usesHelper())
|
||
}
|
||
}
|
||
```
|
||
`spyk` оборачивает **существующий** объект: незастабованные методы работают как раньше, застабованные подменяются. Это нужно, когда тестируется класс, часть методов которого хочется изолировать.
|
||
|
||
Приватные методы требуют отдельного флага:
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.spyk
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
class WithPrivate {
|
||
private fun secret(): Int = 1
|
||
fun public(): Int = secret() * 10
|
||
}
|
||
|
||
class PrivateCallsTest {
|
||
@Test
|
||
fun `приватный метод стабуется по имени`() {
|
||
val spy = spyk(WithPrivate(), recordPrivateCalls = true)
|
||
every { spy["secret"]() } returns 5
|
||
assertEquals(50, spy.public())
|
||
}
|
||
}
|
||
```
|
||
Доступ по строковому имени не проверяется компилятором - при переименовании метода тест молча перестанет стабовать нужное. Это крайняя мера; обычно приватный метод стоит вынести в отдельный класс.
|
||
|
||
## Kotlin-специфичное мокирование
|
||
Здесь MockK даёт то, чего Mockito без расширений не умеет.
|
||
|
||
### Финальные классы
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.mockk
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
class FinalRepo {
|
||
fun load(id: Int): String = "реальный $id"
|
||
}
|
||
|
||
class FinalClassTest {
|
||
@Test
|
||
fun `финальный класс мокается без настройки`() {
|
||
val repo = mockk<FinalRepo>()
|
||
every { repo.load(1) } returns "мок"
|
||
assertEquals("мок", repo.load(1))
|
||
}
|
||
}
|
||
```
|
||
В Kotlin классы финальны по умолчанию, поэтому это основной случай, а не исключение. Подробнее: [Наследование](../ООП/Наследование.md)
|
||
|
||
### object и companion object
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.mockkObject
|
||
import io.mockk.unmockkObject
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
object Config {
|
||
fun url(): String = "prod://db"
|
||
}
|
||
|
||
class WithCompanion {
|
||
companion object {
|
||
fun create(): String = "реальный"
|
||
}
|
||
}
|
||
|
||
class ObjectMockTest {
|
||
@Test
|
||
fun `mockkObject подменяет синглтон`() {
|
||
mockkObject(Config)
|
||
every { Config.url() } returns "test://mem"
|
||
assertEquals("test://mem", Config.url())
|
||
|
||
unmockkObject(Config)
|
||
assertEquals("prod://db", Config.url())
|
||
}
|
||
|
||
@Test
|
||
fun `companion мокается как объект`() {
|
||
mockkObject(WithCompanion.Companion)
|
||
every { WithCompanion.create() } returns "мок"
|
||
assertEquals("мок", WithCompanion.create())
|
||
unmockkObject(WithCompanion.Companion)
|
||
}
|
||
}
|
||
```
|
||
`mockkObject` оборачивает **реальный** экземпляр - это spy, а не чистый мок: незастабованные методы продолжают работать. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
||
|
||
### Функции верхнего уровня и расширения
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.mockkStatic
|
||
import io.mockk.unmockkStatic
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
fun topLevel(x: Int): Int = x * 2
|
||
|
||
fun String.shout(): String = "$this!"
|
||
|
||
class StaticMockTest {
|
||
@Test
|
||
fun `функция верхнего уровня`() {
|
||
mockkStatic(::topLevel)
|
||
every { topLevel(any()) } returns 100
|
||
assertEquals(100, topLevel(1))
|
||
|
||
unmockkStatic(::topLevel)
|
||
assertEquals(2, topLevel(1))
|
||
}
|
||
|
||
@Test
|
||
fun `функция-расширение`() {
|
||
mockkStatic(String::shout)
|
||
every { any<String>().shout() } returns "замок"
|
||
assertEquals("замок", "привет".shout())
|
||
|
||
unmockkStatic(String::shout)
|
||
assertEquals("привет!", "привет".shout())
|
||
}
|
||
}
|
||
```
|
||
Обе компилируются в статические методы класса файла, поэтому используется `mockkStatic`. Ссылка на функцию (`::topLevel`) предпочтительнее строки с именем класса - её проверяет компилятор. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||
|
||
### Конструкторы
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.mockkConstructor
|
||
import io.mockk.unmockkConstructor
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
class FinalRepo {
|
||
fun load(id: Int): String = "реальный $id"
|
||
}
|
||
|
||
class ConstructorMockTest {
|
||
@Test
|
||
fun `mockkConstructor перехватывает создание объектов`() {
|
||
mockkConstructor(FinalRepo::class)
|
||
every { anyConstructed<FinalRepo>().load(any()) } returns "из конструктора"
|
||
|
||
assertEquals("из конструктора", FinalRepo().load(5))
|
||
|
||
unmockkConstructor(FinalRepo::class)
|
||
assertEquals("реальный 5", FinalRepo().load(5))
|
||
}
|
||
}
|
||
```
|
||
Это обходной путь для кода, создающего зависимости внутри себя. Нужда в `mockkConstructor` - сигнал, что зависимость стоит передавать через конструктор.
|
||
|
||
### suspend-функции
|
||
```kotlin
|
||
import io.mockk.coEvery
|
||
import io.mockk.coVerify
|
||
import io.mockk.just
|
||
import io.mockk.Runs
|
||
import io.mockk.mockk
|
||
import kotlinx.coroutines.delay
|
||
import kotlinx.coroutines.test.runTest
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
interface AsyncRepo {
|
||
suspend fun fetch(id: Int): String
|
||
suspend fun saveAll(items: List<String>)
|
||
}
|
||
|
||
class SuspendMockTest {
|
||
@Test
|
||
fun `coEvery и coVerify`() = runTest {
|
||
val repo = mockk<AsyncRepo>()
|
||
coEvery { repo.fetch(1) } returns "данные"
|
||
coEvery { repo.saveAll(any()) } just Runs
|
||
|
||
assertEquals("данные", repo.fetch(1))
|
||
repo.saveAll(listOf("a"))
|
||
|
||
coVerify(exactly = 1) { repo.fetch(1) }
|
||
coVerify { repo.saveAll(listOf("a")) }
|
||
}
|
||
|
||
@Test
|
||
fun `coAnswers может приостанавливаться`() = runTest {
|
||
val repo = mockk<AsyncRepo>()
|
||
coEvery { repo.fetch(any()) } coAnswers {
|
||
delay(1000)
|
||
"после задержки"
|
||
}
|
||
assertEquals("после задержки", repo.fetch(1))
|
||
}
|
||
}
|
||
```
|
||
Для `suspend`-функций нужны `coEvery`, `coVerify`, `coAnswers` - обычные `every` и `verify` не могут вызвать приостанавливаемую лямбду. `delay` внутри `coAnswers` под `runTest` виртуализируется и не замедляет тест. Подробнее: [Тестирование корутин](Тестирование%20корутин.md)
|
||
|
||
## Аннотации
|
||
```kotlin
|
||
import io.mockk.MockKAnnotations
|
||
import io.mockk.clearAllMocks
|
||
import io.mockk.every
|
||
import io.mockk.impl.annotations.InjectMockKs
|
||
import io.mockk.impl.annotations.MockK
|
||
import io.mockk.impl.annotations.RelaxedMockK
|
||
import io.mockk.verify
|
||
import org.junit.jupiter.api.AfterEach
|
||
import org.junit.jupiter.api.BeforeEach
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertTrue
|
||
|
||
data class User(val id: Int, val name: String)
|
||
interface UserRepo { fun findById(id: Int): User? }
|
||
interface Mailer { fun send(to: String, body: String) }
|
||
|
||
class Notifier(private val repo: UserRepo, private val mailer: Mailer) {
|
||
fun notify(id: Int): Boolean {
|
||
val user = repo.findById(id) ?: return false
|
||
mailer.send(user.name, "привет")
|
||
return true
|
||
}
|
||
}
|
||
|
||
class AnnotationsTest {
|
||
@MockK lateinit var repo: UserRepo
|
||
@RelaxedMockK lateinit var mailer: Mailer
|
||
@InjectMockKs lateinit var notifier: Notifier
|
||
|
||
@BeforeEach fun setUp() = MockKAnnotations.init(this)
|
||
@AfterEach fun tearDown() = clearAllMocks()
|
||
|
||
@Test
|
||
fun `зависимости подставляются автоматически`() {
|
||
every { repo.findById(1) } returns User(1, "Иван")
|
||
|
||
assertTrue(notifier.notify(1))
|
||
verify { mailer.send("Иван", "привет") }
|
||
}
|
||
}
|
||
```
|
||
`@InjectMockKs` ищет моки по типу и передаёт их в конструктор. Это избавляет от ручного `Notifier(repo, mailer)` - полезно, когда конструктор большой и часто меняется.
|
||
|
||
JUnit 5 extension делает `init` сам:
|
||
```kotlin
|
||
import io.mockk.every
|
||
import io.mockk.impl.annotations.MockK
|
||
import io.mockk.junit5.MockKExtension
|
||
import org.junit.jupiter.api.extension.ExtendWith
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
data class User(val id: Int, val name: String)
|
||
interface UserRepo { fun findById(id: Int): User? }
|
||
|
||
@ExtendWith(MockKExtension::class)
|
||
class ExtensionTest {
|
||
@MockK lateinit var repo: UserRepo
|
||
|
||
@Test
|
||
fun `extension инициализирует моки`() {
|
||
every { repo.findById(2) } returns User(2, "Пётр")
|
||
assertEquals("Пётр", repo.findById(2)?.name)
|
||
}
|
||
}
|
||
```
|
||
| Аннотация | Что делает |
|
||
|---|---|
|
||
| `@MockK` | строгий мок |
|
||
| `@RelaxedMockK` | `relaxed = true` |
|
||
| `@SpyK` | `spyk` существующего значения |
|
||
| `@InjectMockKs` | создаёт объект, подставляя моки |
|
||
| `@OverrideMockKs` | как `@InjectMockKs`, но перезаписывает уже заданные поля |
|
||
| `@ExtendWith(MockKExtension::class)` | автоматический `init` и `unmockk` |
|
||
|
||
`MockKExtension` также понимает `@MockKExtension.ConfirmVerification` и `@MockKExtension.CheckUnnecessaryStub` на классе - включает соответствующие проверки для всех тестов.
|
||
|
||
## Сброс состояния
|
||
```kotlin
|
||
import io.mockk.clearMocks
|
||
import io.mockk.every
|
||
import io.mockk.mockk
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
import kotlin.test.assertTrue
|
||
|
||
data class User(val id: Int, val name: String)
|
||
interface UserRepo { fun findAll(): List<User> }
|
||
|
||
class ClearTest {
|
||
@Test
|
||
fun `clearMocks снимает стабы и историю вызовов`() {
|
||
val repo = mockk<UserRepo>()
|
||
every { repo.findAll() } returns listOf(User(1, "x"))
|
||
assertEquals(1, repo.findAll().size)
|
||
|
||
clearMocks(repo)
|
||
// стаб снят - вызов снова бросает
|
||
val e = runCatching { repo.findAll() }.exceptionOrNull()
|
||
assertTrue(e != null)
|
||
}
|
||
}
|
||
```
|
||
| Функция | Что сбрасывает |
|
||
|---|---|
|
||
| `clearMocks(a, b)` | стабы и записанные вызовы указанных моков |
|
||
| `clearMocks(m, answers = false)` | только историю вызовов, стабы остаются |
|
||
| `clearAllMocks()` | все моки, включая `mockkObject` и `mockkStatic` |
|
||
| `unmockkObject(obj)`, `unmockkStatic(...)`, `unmockkConstructor(...)` | снимают конкретную подмену |
|
||
| `unmockkAll()` | снимает все глобальные подмены |
|
||
|
||
Статические подмены и `mockkObject` **глобальны на весь JVM-процесс**, поэтому их обязательно снимать: иначе следующий тест в том же прогоне получит подменённый `object`.
|
||
|
||
## Когда MockK, когда нет
|
||
**Стоит**, если: код на Kotlin с финальными классами, есть `object` и функции верхнего уровня, нужны моки `suspend`-функций, хочется DSL вместо `when(...).thenReturn(...)`.
|
||
|
||
**Не стоит**, если: тестируемый код легко собрать вручную (fake-реализация интерфейса часто читается лучше мока), или проект смешанный и в нём уже есть Mockito с настроенным `mockito-inline`.
|
||
|
||
Fake вместо мока:
|
||
```kotlin
|
||
import kotlin.test.Test
|
||
import kotlin.test.assertEquals
|
||
|
||
data class User(val id: Int, val name: String)
|
||
|
||
interface UserRepo {
|
||
fun findById(id: Int): User?
|
||
fun save(user: User): User
|
||
}
|
||
|
||
class FakeUserRepo : UserRepo {
|
||
private val storage = mutableMapOf<Int, User>()
|
||
override fun findById(id: Int) = storage[id]
|
||
override fun save(user: User) = user.also { storage[it.id] = it }
|
||
}
|
||
|
||
class FakeTest {
|
||
@Test
|
||
fun `fake хранит состояние и читается без DSL`() {
|
||
val repo = FakeUserRepo()
|
||
repo.save(User(1, "Иван"))
|
||
assertEquals("Иван", repo.findById(1)?.name)
|
||
}
|
||
}
|
||
```
|
||
Для интерфейса с состоянием fake обычно короче набора `every`-стабов и не ломается при изменении порядка вызовов. MockK нужнее там, где важно именно проверить взаимодействие.
|
||
|
||
> [!info] Отличия от Java
|
||
> | Mockito | MockK |
|
||
> |---|---|
|
||
> | `mock(Repo.class)` | `mockk<Repo>()` |
|
||
> | `when(m.f()).thenReturn(x)` | `every { m.f() } returns x` |
|
||
> | `verify(m).f()` | `verify { m.f() }` |
|
||
> | `verify(m, times(2)).f()` | `verify(exactly = 2) { m.f() }` |
|
||
> | финальные классы требуют `mockito-inline` | работают из коробки |
|
||
> | статику мокает только `mockStatic` с try-with-resources | `mockkStatic(::f)` |
|
||
> | `suspend` недоступны | `coEvery`, `coVerify` |
|
||
> | `ArgumentCaptor<T>` + `getValue()` | `slot<T>()` + `captured` |
|
||
> | `@Mock` + `MockitoAnnotations.openMocks` | `@MockK` + `MockKAnnotations.init` |
|
||
>
|
||
> **Финальность по умолчанию - причина существования MockK.** В Java классы открыты, поэтому Mockito годами обходился генерацией подклассов. В Kotlin класс без `open` финален, и подкласс создать нельзя - старый Mockito на таком коде просто падал. MockK использует инструментацию байт-кода через агент, поэтому финальность ему безразлична. Mockito догнал это `mockito-inline`, но подключать его нужно отдельно. Подробнее: [Наследование](../ООП/Наследование.md)
|
||
>
|
||
> **`object` и функции верхнего уровня не имеют аналога в Java.** Java-код обычно внедряет зависимости через конструктор, и статический вызов считается плохим стилем, который мокать не нужно. В Kotlin `object` - идиоматичный синглтон, а функция верхнего уровня - обычный способ написать утилиту; оба встречаются в коде, который требуется тестировать. `mockkObject` и `mockkStatic` закрывают этот разрыв, но ценой глобального состояния: подмена живёт до `unmockk`. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
||
>
|
||
> **DSL на лямбдах вместо цепочек методов.** `when(m.f()).thenReturn(x)` вызывает `m.f()` по-настоящему, а Mockito перехватывает вызов постфактум - отсюда странности с `void`-методами (`doReturn().when()`). В MockK `every { }` - это лямбда, которую библиотека выполняет в режиме записи, поэтому `Unit`-функции, `suspend`-функции и свойства обрабатываются одинаково. Подробнее: [Лямбды и функциональные типы](../Синтаксис%20и%20типы/Работа%20с%20данными/Лямбды%20и%20функциональные%20типы.md)
|
||
>
|
||
> **`null` в Mockito-моках был постоянной проблемой; в Kotlin он не компилируется.** Mockito по умолчанию возвращает `null` для незастабованных методов, и Java-код падал с NPE внутри теста. MockK вместо этого бросает `MockKException` с понятным сообщением либо, в `relaxed`-режиме, возвращает непустое значение по умолчанию. Строгая типизация `null` в Kotlin делает первый вариант единственно корректным. Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
|
||
>
|
||
> Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
|
||
|
||
> [!warning] Подводные камни
|
||
> **Порядок стабов определяет приоритет.** Последний заданный `every` выигрывает при совпадении аргументов. `every { repo.findById(1) } returns a` после `every { repo.findById(any()) } returns b` работает как ожидается, но в обратном порядке `any()` перехватит и `1` - проверено: вызов `findById(1)` вернёт значение из `any()`.
|
||
>
|
||
> **`relaxed = true` маскирует забытые стабы.** Для ссылочных типов возвращается дочерний мок, а не `null`, поэтому `repo.findById(1)?.name` даст не исключение, а бессмысленную строку. Для тестов, где важна полнота стабов, используйте строгий `mockk()` или `relaxUnitFun = true`.
|
||
>
|
||
> **`mockkObject` и `mockkStatic` глобальны и переживают тест.** Подмена живёт до `unmockkObject` / `unmockkStatic` / `unmockkAll` в пределах JVM, поэтому забытый вызов портит последующие тесты - причём непредсказуемо, так как порядок тестов не гарантирован. Всегда снимайте подмену в `@AfterEach` или через `MockKExtension`.
|
||
>
|
||
> **`mockkObject` - это spy, а не чистый мок.** Незастабованные методы `object` продолжают выполнять реальный код, в том числе обращаться к сети и базе. Для полной изоляции нужно застабовать каждый используемый метод.
|
||
>
|
||
> **`every` и `verify` не работают с `suspend`.** Нужны `coEvery` и `coVerify`; обычная форма даст ошибку компиляции или зависнет. Исключение - `coAnswers` вместо `answers`, когда внутри ответа есть `delay`.
|
||
>
|
||
> **`slot.captured` до первого вызова бросает `IllegalStateException`.** Проверять захваченное значение можно только после того, как мок был вызван; при `verify(exactly = 0)` обращение к `captured` всегда провалится.
|
||
>
|
||
> **`slot` хранит только последний аргумент.** При нескольких вызовах предыдущие значения теряются молча. Для проверки всех вызовов нужен `capture(mutableListOf<T>())`.
|
||
>
|
||
> **`verifySequence` ломается от любого лишнего вызова.** Он требует ровно перечисленных вызовов, включая те, которые делает сам тестируемый код внутри. Для проверки только порядка нужных вызовов берите `verifyOrder`.
|
||
>
|
||
> **`confirmVerified` и `checkUnnecessaryStub` бросают `AssertionError`.** Это удобно (похоже на провал утверждения), но означает, что `runCatching { ... }.onFailure { }` в тесте проглотит ошибку вместе с реальными провалами.
|
||
>
|
||
> **`spyk` с `recordPrivateCalls` использует строковое имя метода.** `spy["secret"]()` не проверяется компилятором: переименование метода оставит тест зелёным, хотя стаб больше не применяется.
|
||
>
|
||
> **Сопоставление идёт по `equals`, поэтому `data class` ведёт себя иначе, чем обычный класс.** `verify { repo.save(User(1, "Иван")) }` проходит для `data class`, потому что `equals` сравнивает поля; для обычного класса без `equals` нужен тот же экземпляр или `refEq`. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
|
||
>
|
||
> **`returnsMany` после исчерпания списка повторяет последнее значение.** Четвёртый вызов при `returnsMany listOf(1, 2, 3)` вернёт `3`, а не бросит исключение - тест на «не больше трёх вызовов» так не напишешь, нужен `verify(exactly = 3)`.
|
||
>
|
||
> **Мокирование `inline`-функций не работает.** Тело `inline`-функции подставляется в место вызова на этапе компиляции, поэтому перехватывать нечего. Это же относится к `inline`-свойствам и функциям с `reified`. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
|
||
>
|
||
> **MockK медленнее Mockito на старте.** Агент инструментирует классы при первом обращении; в больших наборах тестов это десятки секунд. Переиспользование моков через `clearMocks` вместо создания новых в каждом тесте заметно помогает.
|
||
>
|
||
> **`@InjectMockKs` подбирает зависимости по типу.** Два мока одного типа в тесте приводят к неопределённому выбору, а конструктор с примитивами и строками получит значения по умолчанию вместо осмысленных. Если инъекция неочевидна - собирайте объект руками.
|
||
|
||
Далее: [Тестирование корутин](Тестирование%20корутин.md)
|