Files
obsidian/Kotlin/Тестирование/MockK.md
T

781 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)