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

37 KiB
Raw Blame History

title, description, tags
title description tags
MockK Моки, стабы, spyk, проверка вызовов, мокирование object, companion, функций верхнего уровня и suspend
kotlin
testing
mockk

MockK - библиотека моков, написанная для Kotlin. Главное её отличие от Mockito в том, что финальные классы, object, функции верхнего уровня и suspend-функции мокаются без дополнительной настройки.

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 { } проверяет вызов.

Подключение

// 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 сам.

Создание моков

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). Это удобно для цепочек, но скрывает забытые стабы.

Задание поведения

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.

Сопоставители аргументов

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.

Захват аргументов

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 короче, когда захваченное значение нужно только для одной проверки.

Проверка вызовов

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 - частичный мок

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 оборачивает существующий объект: незастабованные методы работают как раньше, застабованные подменяются. Это нужно, когда тестируется класс, часть методов которого хочется изолировать.

Приватные методы требуют отдельного флага:

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 без расширений не умеет.

Финальные классы

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 классы финальны по умолчанию, поэтому это основной случай, а не исключение. Подробнее: Наследование

object и companion object

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

Функции верхнего уровня и расширения

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 в байт-коде

Конструкторы

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-функции

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 виртуализируется и не замедляет тест. Подробнее: Тестирование корутин

Аннотации

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 сам:

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 на классе - включает соответствующие проверки для всех тестов.

Сброс состояния

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 вместо мока:

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, но подключать его нужно отдельно. Подробнее: Наследование

object и функции верхнего уровня не имеют аналога в Java. Java-код обычно внедряет зависимости через конструктор, и статический вызов считается плохим стилем, который мокать не нужно. В Kotlin object - идиоматичный синглтон, а функция верхнего уровня - обычный способ написать утилиту; оба встречаются в коде, который требуется тестировать. mockkObject и mockkStatic закрывают этот разрыв, но ценой глобального состояния: подмена живёт до unmockk. Подробнее: Object и companion object

DSL на лямбдах вместо цепочек методов. when(m.f()).thenReturn(x) вызывает m.f() по-настоящему, а Mockito перехватывает вызов постфактум - отсюда странности с void-методами (doReturn().when()). В MockK every { } - это лямбда, которую библиотека выполняет в режиме записи, поэтому Unit-функции, suspend-функции и свойства обрабатываются одинаково. Подробнее: Лямбды и функциональные типы

null в Mockito-моках был постоянной проблемой; в Kotlin он не компилируется. Mockito по умолчанию возвращает null для незастабованных методов, и Java-код падал с NPE внутри теста. MockK вместо этого бросает MockKException с понятным сообщением либо, в relaxed-режиме, возвращает непустое значение по умолчанию. Строгая типизация null в Kotlin делает первый вариант единственно корректным. Подробнее: Null-безопасность

Сравнить: Reflection API в Java

[!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

returnsMany после исчерпания списка повторяет последнее значение. Четвёртый вызов при returnsMany listOf(1, 2, 3) вернёт 3, а не бросит исключение - тест на «не больше трёх вызовов» так не напишешь, нужен verify(exactly = 3).

Мокирование inline-функций не работает. Тело inline-функции подставляется в место вызова на этапе компиляции, поэтому перехватывать нечего. Это же относится к inline-свойствам и функциям с reified. Подробнее: Reified и inline

MockK медленнее Mockito на старте. Агент инструментирует классы при первом обращении; в больших наборах тестов это десятки секунд. Переиспользование моков через clearMocks вместо создания новых в каждом тесте заметно помогает.

@InjectMockKs подбирает зависимости по типу. Два мока одного типа в тесте приводят к неопределённому выбору, а конструктор с примитивами и строками получит значения по умолчанию вместо осмысленных. Если инъекция неочевидна - собирайте объект руками.

Далее: Тестирование корутин