MockK — е Kotlin-first рамка за създаване на mock-обекти, проектирана специално за екосистемата на Kotlin като отчита неговите езикови особености: корутини, extension функции, data class и sealed class. За разлика от Mockito, който е пренесен на Kotlin от Java, MockK от самото начало е проектиран за Kotlin синтаксис и не изисква допълнителни приложения за работа с final класове. Според данните на MockK.io, библиотеката се използва в повече от 40% от Kotlin проектите с модулно тестване.
Основни моменти
MockK — е библиотека за създаване на mock-обекти, написана на Kotlin и оптимизирана за неговата синтаксис. Тя решава същите задачи като Mockito — изолация на тествания код от зависимости — но го прави с помощта на Kotlin-специфични конструкции: ламбда, DSL, reified generics и suspend функции.
Основното предимство на MockK преди пренесените решения — воденствената подкрепа за Kotlin. В Mockito, mockването на final клас изисква opt-in (mockito-inline), а на статични методи — mockStatic. MockK поддържа това по подразбиране, тъй като Kotlin класовете по подразбиране са final и заобиколяването на това ограничение е вградено в архитектурата на библиотеката.
Версия 1.13.12 (2024) — стабилно издание, което поддържа Kotlin 2.0, K2 компилатор и мултиплатформени проекти (KMP). MockK работи също с Kotlin/Native и Kotlin/JS, което го прави единственият избор за KMP проекти, където нито Mockito, нито EasyMock не са приложими.
MockK е проектиран, като се отчита спецификата на Kotlin и използва езиковите възможности — reified generics, DSL с ламбда, inline функции — за да предостави съкратен и типово безопасен API без загуба на производителност.
Механизмът на MockK се базира на инструментация на байткод чрез библиотеката ByteBuddy (като Mockito), но ги обагря в Kotlin-приятелски DSL. Вместо вериги от when().thenReturn(), MockK използва ламбда блокове every { } и coEvery { }, които изглеждат като естествено разширение на езика. Под капака, MockK прехварля повикането внутри на ламбдата, анализира метода и аргументите чрез рефлексия и ги съпоставя с записаните правила на stubbing.
Блокът every { mock.method() } returns value се чете като „всякакъга, когато методът се повика, връщай стойността”. Такава декларативен синтаксис е по-близък до Kotlin стила и премахва обръкването с реда на аргументите в when(). Благодарение на reified generics на Kotlin, типът на mock-а се извежда автоматично без изрично посочване на класа.
val repository = mockk<UserRepository>()
// Stubbing: всяко повикане на findById(1) връща потребител
every { repository.findById(1) } returns User("Alice")
// Повикане и проверка
val result = repository.findById(1)
assertEquals("Alice", result.name)
За разлика от Mockito, където всеки метод трябва да се настрои изрично, MockK поддържа relaxed mock — mock, който връща „разумни” подразбираеми стойности за всякакъв метод: празен списък за List, 0 за Int, празен низ за String. Това драстично намалява количеството на подготвителен код.
// Relaxed mock — всички методи връщат подразбираеми стойности
val api = mockk<ApiService>(relaxed = true)
// Не изисква stubbing — ще връне празен списък
println(api.getUsers()) // []
MockK предлага няколко начина за създаване на mock-обекти: mockk<T>() за строг mock (всеки метод трябва да бъде изрично настроен), mockk<T>(relaxed = true) за релаксиран mock и spyk(obj) за създаване на шпион върху реален обект.
| Функция | Тип | Поведение без stubbing |
|---|---|---|
| mockk() | Строг mock | Хвърля изключение при повикане на нестубнат метод |
| mockk(relaxed = true) | Релаксиран mock | Връща подразбираема стойност |
| spyk() | Шпион | Повика реалния метод, ако няма конфигуриран stub |
| slot() | Argument Captor | Улавя аргумента за проверка |
Изборът между строг и релаксиран mock зависи от контекста. Строгият mock гарантира, че тестът не използва методи, чието поведение не е дефинирано — това повишава надеждността. Релаксираният mock е удобен за бързо прототипиране на тестове, където не всички зависимости са важни. На практика се препоръчва да се започне със строг mock и да се премине към relaxed само когато stubbing ът заема повече редове от самия тест.
Every блокът — е централната конструкция на stubbing в MockK. Внутри ламбдата се описва повикане на метод с конкретни аргументи, след което стойността се връща чрез returns, изключението се хвърля чрез throws или отговорът се изчислява чрез answers.
MockK поддържа всички сценарии, необходими за тестване: връщане на стойност, хвърляне на изключение, изчисляване на отговор на базата на аргументите, няколко отговора по ред (последователност на повикания).
// Връщане на стойност
every { repo.findById(1) } returns User("Alice")
// Хвърляне на изключение
every { repo.findById(999) } throws NotFoundException()
// Динамичен отговор
every { repo.save(any()) } answers {
val user = firstArg<User>()
user.copy(id = 42)
}
// Последователност на отговори
every { repo.findAll() } returnsMany listOf(
listOf(User("Alice")),
listOf(User("Bob")),
emptyList()
)
Verify в MockK е аналогичен на Mockito.verify() по смисъл, но използва Kotlin DSL: verify { mock.method() }. За suspend функции се използва coVerify { mock.suspendMethod() }, който работи коректно с корутините и не изисква специален runner.
MockK поддържа същите модификатори като Mockito: exactly(1), atLeast(2), atMost(5), wasNot(Called). Синтаксисът е минималистичен — модификаторът се предава като първи аргумент в verify { }.
// Проверка: методът е повикан точно 1 път
verify(exactly = 1) { repo.save(any()) }
// Проверка на реда на повиканията
verifySequence {
repo.save(any())
repo.flush()
}
// coVerify за suspend функции
coVerify { api.fetchUsers() }
За проверка на аргументите се използва slot() — аналог на ArgumentCaptor. Slot-ът се декларира преди повикането, предава се на every или verify, и след изпълнението на теста съдържа уловената стойност.
val userSlot = slot<User>()
verify { repo.save(capture(userSlot)) }
assertEquals("Alice", userSlot.captured.name)
MockK предоставя анотациите @MockK и @RelaxedMockK за създаване на mockове чрез инициализация в JUnit 5. Разширението MockKExtension автоматично създава mockове преди всяк тест и изчиства след това — аналогично на MockitoExtension, но с подкрепа за relaxed режим.
Анотацията @InjectMockKs (или алтернативата @MockK с изрично създаване на обект) инжектира mockовете в тествания инстанс. Това намалява boilerplate кода и прави тестовия код по-чист.
@ExtendWith(MockKExtension::class)
class UserServiceTest {
@MockK
lateinit var repository: UserRepository
@InjectMockKs
lateinit var service: UserService
@Test
fun `getUser returns user from repository`() {
every { repository.findById(1) } returns User("Alice")
assertEquals("Alice", service.getUser(1)?.name)
}
}
Изборът между MockK и Mockito зависи от състава на екипа и типа на проекта. Mockito има по-голяма екосистема, повече примери и интеграции, но MockK предоставя по-чист Kotlin синтаксис и воденствена подкрепа за езиковите функции. За нови Kotlin проекти MockK се препоръчва като по-идиоматично решение.
| Критерий | MockK | Mockito |
|---|---|---|
| Синтаксис | Kotlin DSL (every, verify) | Java стил (when, thenReturn) |
| Корутини | coEvery, coVerify (воденствено) | Изисква допълнителни библиотеки |
| Final class | Поддържано по подразбиране | Изисква mockito-inline |
| KMP | Поддържано | Не е поддържано |
| Relaxed mock | Вграден | Няма аналог |
| Популярност | Растяща в Kotlin общността | Доминира в Java и хибридни проекти |
За проекти на чист Kotlin (без Java класове) MockK е предпочитан: по-малко boilerplate, воденствена подкрепа за корутините, без изненада с final класове. За хибридни проекти или екипове с Java произход Mockito остава работящ вариант — и двете библиотеки могат да се използват в един проект чрез различни модули. При миграция от Mockito към MockK е достатъчно да се заменят @Mock анотациите с @MockK и да се препишат блоковете when().thenReturn() в формат every { }.
Често задавани въпроси
Relaxed mock връща подразбираеми стойности за всички нестубнати методи (празен списък, 0, null), без да хвърля изключения. Обикновеният (strict) mock изисква изричен stubbing на всеки метод — иначе тестът се проваля. Relaxed mock е удобен за бързи тестове, strict — за надеждни.
MockK поддържа mockване на extension функции чрез mockkStatic(). Това е възможно, защото extension функциите в Kotlin са статични методи с първи параметър-приемник. За всяка extension функция трябва да се посочи класът, в който е декларирана.
Да, MockK поддържа Kotlin Multiplatform (KMP) за общ код. На платформите JVM, Native и JS може да се използва общият API: mockk(), every, verify. Това прави MockK единственият избор за KMP проекти, където Mockito не работи.
Използвайте verifySequence { } — блок, в който повиканията са посочени строго в очаквания ред. Ако действителният ред се различава, verifySequence ще хвърли изключение с посочване на първото несъвпадащо повикане.
Да, технически е възможно, но не се препоръчва. Конфликти могат да възникнат на ниво инструментация на байткод (ByteBuddy vs mockito-inline). Ако проектът вече използва Mockito, миграцията към MockK може да бъде постепенна чрез изолация на модулите.
Заключение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също