--- 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() every { repo.findById(1) } returns User(1, "Иван") assertEquals("Иван", repo.findById(1)?.name) verify { repo.findById(1) } } } ``` Три шага: `mockk()` создаёт заглушку, `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 fun clear() } class MockModesTest { @Test fun `строгий мок требует стаба на каждый вызов`() { val repo = mockk() every { repo.count() } returns 7 assertEquals(7, repo.count()) // repo.name() бросил бы MockKException } @Test fun `relaxed возвращает значения по умолчанию`() { val repo = mockk(relaxed = true) assertEquals(0, repo.count()) assertEquals("", repo.name()) assertEquals(false, repo.flag()) assertEquals(emptyList(), repo.items()) } @Test fun `relaxUnitFun ослабляет только Unit-методы`() { val repo = mockk(relaxUnitFun = true) repo.clear() // ок // repo.count() всё ещё бросит MockKException } @Test fun `имя мока попадает в сообщение об ошибке`() { val repo = mockk(name = "репозиторий") val message = runCatching { repo.count() }.exceptionOrNull()?.message assertEquals(true, message?.contains("репозиторий")) } } ``` | Форма | Поведение на нестабованном вызове | |---|---| | `mockk()` | `MockKException: no answer found` | | `mockk(relaxed = true)` | значение по умолчанию: `0`, `""`, `false`, пустая коллекция, мок для ссылочного типа | | `mockk(relaxUnitFun = true)` | `Unit`-методы проходят, остальные бросают | | `mockk(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() every { repo.count() } returnsMany listOf(1, 2, 3) // последнее значение повторяется для всех дальнейших вызовов assertEquals(listOf(1, 2, 3, 3), List(4) { repo.count() }) } @Test fun `answers видит аргументы вызова`() { val repo = mockk() every { repo.findById(any()) } answers { User(firstArg(), "пользователь ${firstArg()}") } assertEquals("пользователь 7", repo.findById(7)?.name) } @Test fun `throws`() { val repo = mockk() every { repo.count() } throws IllegalStateException("нет соединения") val e = runCatching { repo.count() }.exceptionOrNull() assertEquals("нет соединения", e?.message) } @Test fun `just Runs для Unit`() { val repo = mockk() every { repo.deleteAll() } just Runs repo.deleteAll() } } ``` | Инфикс | Что делает | |---|---| | `returns x` | всегда одно значение | | `returnsMany listOf(...)` | значения по порядку, последнее повторяется | | `answers { }` | лямбда с доступом к `firstArg()`, `secondArg()`, `arg(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() 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()` | конкретный тип | | `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() val saved = slot() every { repo.save(capture(saved)) } answers { saved.captured } repo.save(User(5, "Анна")) assertEquals("Анна", saved.captured.name) } @Test fun `список собирает все вызовы`() { val repo = mockk(relaxed = true) val ids = mutableListOf() 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(relaxed = true) repo.save(User(9, "Олег")) verify { repo.save(withArg { assertTrue(it.name.startsWith("О")) }) } } } ``` `slot()` хранит одно значение - обращение к `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 } class VerifyTest { @Test fun `количество вызовов`() { val repo = mockk(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(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(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() 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() 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().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().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) } class SuspendMockTest { @Test fun `coEvery и coVerify`() = runTest { val repo = mockk() 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() 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 } class ClearTest { @Test fun `clearMocks снимает стабы и историю вызовов`() { val repo = mockk() 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() 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()` | > | `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` + `getValue()` | `slot()` + `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())`. > > **`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)