37 KiB
title, description, tags
| title | description | tags | |||
|---|---|---|---|---|---|
| MockK | Моки, стабы, spyk, проверка вызовов, мокирование object, companion, функций верхнего уровня и suspend |
|
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 xverify(m).f()verify { m.f() }verify(m, times(2)).f()verify(exactly = 2) { m.f() }финальные классы требуют mockito-inlineработают из коробки статику мокает только mockStaticс try-with-resourcesmockkStatic(::f)suspendнедоступныcoEvery,coVerifyArgumentCaptor<T>+getValue()slot<T>()+captured@Mock+MockitoAnnotations.openMocks@MockK+MockKAnnotations.initФинальность по умолчанию - причина существования MockK. В Java классы открыты, поэтому Mockito годами обходился генерацией подклассов. В Kotlin класс без
openфинален, и подкласс создать нельзя - старый Mockito на таком коде просто падал. MockK использует инструментацию байт-кода через агент, поэтому финальность ему безразлична. Mockito догнал этоmockito-inline, но подключать его нужно отдельно. Подробнее: Наследование
objectи функции верхнего уровня не имеют аналога в Java. Java-код обычно внедряет зависимости через конструктор, и статический вызов считается плохим стилем, который мокать не нужно. В Kotlinobject- идиоматичный синглтон, а функция верхнего уровня - обычный способ написать утилиту; оба встречаются в коде, который требуется тестировать.mockkObjectиmockkStaticзакрывают этот разрыв, но ценой глобального состояния: подмена живёт доunmockk. Подробнее: Object и companion objectDSL на лямбдах вместо цепочек методов.
when(m.f()).thenReturn(x)вызываетm.f()по-настоящему, а Mockito перехватывает вызов постфактум - отсюда странности сvoid-методами (doReturn().when()). В MockKevery { }- это лямбда, которую библиотека выполняет в режиме записи, поэтому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 и inlineMockK медленнее Mockito на старте. Агент инструментирует классы при первом обращении; в больших наборах тестов это десятки секунд. Переиспользование моков через
clearMocksвместо создания новых в каждом тесте заметно помогает.
@InjectMockKsподбирает зависимости по типу. Два мока одного типа в тесте приводят к неопределённому выбору, а конструктор с примитивами и строками получит значения по умолчанию вместо осмысленных. Если инъекция неочевидна - собирайте объект руками.
Далее: Тестирование корутин