Mock — це об'єкт-замінник, який імітує поведінку реального компонента та дозволяє перевіряти взаємодію з ним. На відміну від Stub, який просто повертає задане значення, Mock фіксує факт виклику методу, передані аргументи та кількість викликів. За даними Mockito (2024), Mock — найпопулярніший тип Test Double в Java та Kotlin-проєктах, що використовується більш ніж у 70% unit-тестів мобільних застосунків.
Головне
Mock — це об'єкт, створюваний фреймворком mocking (Mockito, MockK, EasyMock), який імітує інтерфейс або клас та записує всі виклики своїх методів. Розробник задає очікування: метод X буде викликано з аргументами Y і поверне Z. Після виконання тесту Mock перевіряє, чи збіглися очікування з реальними викликами.
Термін походить з театральної метафори Test Doubles: Mock — це «імітатор», який не просто стоїть на сцені (як Dummy), а виконує роль і перевіряє, чи правильно з ним взаємодіяли. Якщо код, що тестується, не викликав метод, який Mock очікував, або викликав з невірними аргументами — тест падає з повідомленням про порушене очікування.
Mock створюється через фабрику фреймворку: mockk<MyInterface>() або Mockito.mock(MyClass.java). Фреймворк генерує proxy-об'єкт, який перехоплює всі виклики методів. Кожен виклик порівнюється з заздалегідь заданими очікуваннями (expectations). Якщо виклик відповідає очікуванню — повертається задане значення. Якщо ні — Mock повертає значення за замовчуванням або викидає виняток, залежно від конфігурації.
Mock обов'язковий, коли код, що тестується, взаємодіє з компонентами, які мають побічні ефекти: відправка даних на сервер, запис до бази даних, логування, analytics, навігація, показ системних діалогів. Без Mock ці взаємодії неможливо перевірити без запуску реальної інфраструктури. За даними Google Testing Blog, Mock — єдиний спосіб перевірити, що застосунок дійсно відправив analytics-подію, без підняття тестового сервера.
Різниця між Mock та Stub — одна з найбільш обговорюваних тем у тестуванні. Обидва типи замінюють реальну залежність, але принципово різними способами.
| Критерій | Mock | Stub |
|---|---|---|
| Головне питання | Чи був викликаний метод? | Який результат повернувся? |
| Верифікація | Поведінки (verify) | Стану (assert) |
| Повернення даних | Опціонально | Обов'язково |
| Приклад | verify(analytics).logEvent(“click”) | assertEquals(5, repository.getCount()) |
| Коли використовувати | Побічні ефекти | Повернення даних |
Простий тест для вибору: задайте собі питання — «якщо я видалю цей рядок коду, тест впаде?». Якщо тест перевіряє значення, що повертається — потрібен Stub (перевірка через assert). Якщо тест перевіряє, що код викликав метод з правильними аргументами — потрібен Mock (перевірка через verify). Ця дихотомія випливає з патерну Command-Query Separation: методи, що змінюють стан (commands), потребують Mock; методи, що повертають дані (queries), потребують Stub.
Вибір між Mockito та MockK — одне з перших рішень при налаштуванні тестового стеку Android-проєкту на Kotlin. Обидві бібліотеки виконують одне завдання, але з різним підходом до Kotlin-специфіки.
Mockito — де-факто стандарт для Java-проєктів. Версія 5.x підтримує mock-об'єкти для final-класів, static-методів та конструкторів завдяки вбудованому MockMaker. Для Kotlin-проєктів Mockito вимагає додаткового налаштування: mockito-kotlin розширення для покращеного синтаксису, mockito-inline для final-класів. Mockito не підтримує Kotlin-корутини та suspend-функції без додаткових адаптерів.
MockK створений спеціально для Kotlin. Він нативно підтримує корутини (coEvery, coVerify), sealed class, data class, object-синглтони та extension-функції. Синтаксис MockK використовує DSL з лямбда-блоками, що виглядає природно в Kotlin-коді. MockK також вміє mock-ити властивості (property mocking) без додаткового налаштування — це важливо для Android-проєктів, що використовують LiveData, StateFlow та Delegates.
// Mockito + mockito-kotlin
val repository = mock<UserRepository>()
whenever(repository.getUser(1)).thenReturn(user)
// MockK
val repository = mockk<UserRepository>()
every { repository.getUser(1) } returns user
Бенчмарки (JVM Benchmark, 2024) показують, що MockK створює mock-об'єкти на 15–20% швидше за Mockito для Kotlin-проєктів завдяки прямій роботі з байткодом Kotlin, а не Java Reflections. Для проєктів з тисячами unit-тестів різниця у швидкості збірки може бути помітною: MockK економить 30–60 секунд на повному прогоні тестів у великих проєктах.
Розглянемо три сценарії: тестування ViewModel з Mock-залежностями, тестування UseCase з перевіркою виклику API та тестування корутин з coVerify.
class ProfileViewModelTest {
private val analytics = mockk<AnalyticsService>()
private val repo = mockk<UserRepository>()
private val vm = ProfileViewModel(repo, analytics)
fun `profile opened logs analytics event`() {
every { analytics.logEvent("profile_opened") } returns Unit
vm.onViewCreated()
verify { analytics.logEvent("profile_opened") }
}
}
class SendMessageUseCaseTest {
private val api = mockk<MessagingApi>()
private val useCase = SendMessageUseCase(api)
fun `send message with correct payload`() = runTest {
val message = Message(text = "Hello", userId = 42)
coEvery { api.sendMessage(any()) } returns MessageResult.Sent("msg_1")
val result = useCase.execute(message)
coVerify {
api.sendMessage(match {
it.text == "Hello" && it.userId == 42
})
}
assertTrue(result is MessageResult.Sent)
}
}
class OrderUseCaseTest {
private val api = mockk<OrderApi>()
private val useCase = OrderUseCase(api)
private val slot = slot<OrderRequest>()
fun `order request contains correct items`() = runTest {
coEvery { api.placeOrder(capture(slot)) } returns OrderResult.Placed("order_1")
useCase.execute(listOf("item_a", "item_b"))
assertEquals(2, slot.captured.items.size)
assertEquals("item_a", slot.captured.items[0])
}
}
Ефективне використання Mock у мобільній розробці вимагає дотримання дисципліни. Порушення цих правил перетворює тести на крихку перешкоду, яка ламається при кожному рефакторингу.
Жорстке правило: Mock створюється тільки для залежностей, що перетинають межу застосунку: API-клієнти, бази даних, файлова система, системні сервіси (LocationManager, BluetoothAdapter, Camera). Внутрішні класи застосунку — domain-сутності, Value Object, прості утиліти — не повинні замінюватися на Mock. Їх поведінка тестується через реальні об'єкти.
Кожен тест повинен містити рівно одну логічну перевірку — або verify (для Mock), або assert (для Stub). Не змішуйте перевірку стану та поведінки в одному тесті. Якщо потрібно перевірити і API-виклик, і результат — створіть два окремі тести з різними назвами. Це правило, відоме як «один assert на тест», сягає рекомендацій Kent Beck (2002).
Крім базового mocking, існують продвинуті техніки, які вирішують специфічні завдання в мобільній розробці: тестування багатопотоковості, перевірка стану Flow та частковий mocking реальних об'єктів.
Spy (або partial mock) дозволяє створити об'єкт, який делегує виклики реальної реалізації, але дозволяє перевизначити окремі методи. У MockK spyk створюється на основі реального екземпляра класу: val repo = spyk(InMemoryUserRepository()). Виклики, для яких задані очікування через every, йдуть через Mock; інші — через реальний об'єкт. Spy особливо корисний для тестування legacy-коду, де ін'єкція залежностей ще не впроваджена, і потрібно перевизначити тільки один метод.
У сучасних Android-проєктах на Jetpack Compose ViewModel експонує стан через StateFlow. MockK дозволяє mock-ити Flow-залежності, а бібліотека Turbine спрощує перевірку емісії. Класичний патерн: MockK для UseCase, що повертає Flow, Turbine для перевірки емісії ViewModel. Цей стек рекомендований документацією Android Testing (Google, 2024) для проєктів на Kotlin Coroutines.
class SearchViewModelTest {
private val searchUseCase = mockk<SearchUseCase>()
private val vm = SearchViewModel(searchUseCase)
fun `search emits results`() = runTest {
coEvery { searchUseCase.search("android") } returns
flowOf(SearchResult.Success(listOf(Item("Android TDD"))))
vm.search("android")
vm.state.test {
val state = awaitItem()
assertTrue(state.items.isNotEmpty())
cancelAndIgnoreRemainingEvents()
}
}
}
Часті запитання
Mock — це концепція, тип Test Double, що перевіряє поведінку. Mockito — це бібліотека для створення Mock-об'єктів у Java та Android. Інші бібліотеки: MockK (Kotlin), EasyMock (Java), Cuckoo (iOS).
Для тестування suspend-функцій з Mock використовуйте MockK (coEvery / coVerify) або Mockito з mockito-kotlin. MockK підтримує корутини нативно: coEvery визначає поведінку suspend-функції, coVerify перевіряє її виклик всередині корутини. Всі suspend-виклики повинні виконуватися всередині runTest (kotlinx-coroutines-test).
Так. У MockK для цього використовується returnsMany: every { api.getData() } returnsMany listOf(response1, response2). У Mockito — ланцюжок thenReturn(value1).thenReturn(value2). Це корисно для тестування поведінки при послідовних викликах з різними відповідями.
У MockK використовуйте анотацію @MockK з полем relaxed = true та викликайте clearMocks(mock) у методі @After. У Mockito — Mockito.reset(mock). Найкраща практика: створювати новий Mock для кожного тесту через @Before, щоб виключити вплив між тестами.
MockK коректно працює із sealed class: every { useCase() } returns Result.Success(data). Mockito не підтримує sealed class безпосередньо, вимагаючи обхідних шляхів. Це одна з причин, чому для Kotlin-проєктів рекомендується MockK замість Mockito.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також