Mock — що це, мок-об'єкти та бібліотеки для тестів

Автор: IT Sectr Опубліковано: 2026-04-10 Час читання: 8 хв

Mock — це об'єкт-замінник, який імітує поведінку реального компонента та дозволяє перевіряти взаємодію з ним. На відміну від Stub, який просто повертає задане значення, Mock фіксує факт виклику методу, передані аргументи та кількість викликів. За даними Mockito (2024), Mock — найпопулярніший тип Test Double в Java та Kotlin-проєктах, що використовується більш ніж у 70% unit-тестів мобільних застосунків.

Головне

  • Mock — об'єкт, що перевіряє взаємодію: які методи викликалися, з якими аргументами та скільки разів
  • Mockito — найпопулярніша бібліотека для створення Mock в Java та Android-проєктах
  • MockK — альтернатива Mockito для Kotlin з нативною підтримкою корутин та sealed class
  • Behavior verification — ключова відмінність Mock від Stub: Mock перевіряє поведінку, а не стан
  • Over-mocking — головний Anti-Pattern: mock повинен бути тільки для зовнішніх залежностей

Що таке Mock?

Mock — це об'єкт, створюваний фреймворком mocking (Mockito, MockK, EasyMock), який імітує інтерфейс або клас та записує всі виклики своїх методів. Розробник задає очікування: метод X буде викликано з аргументами Y і поверне Z. Після виконання тесту Mock перевіряє, чи збіглися очікування з реальними викликами.

Термін походить з театральної метафори Test Doubles: Mock — це «імітатор», який не просто стоїть на сцені (як Dummy), а виконує роль і перевіряє, чи правильно з ним взаємодіяли. Якщо код, що тестується, не викликав метод, який Mock очікував, або викликав з невірними аргументами — тест падає з повідомленням про порушене очікування.

Як працює Mock

Mock створюється через фабрику фреймворку: mockk<MyInterface>() або Mockito.mock(MyClass.java). Фреймворк генерує proxy-об'єкт, який перехоплює всі виклики методів. Кожен виклик порівнюється з заздалегідь заданими очікуваннями (expectations). Якщо виклик відповідає очікуванню — повертається задане значення. Якщо ні — Mock повертає значення за замовчуванням або викидає виняток, залежно від конфігурації.

Коли Mock необхідний

Mock обов'язковий, коли код, що тестується, взаємодіє з компонентами, які мають побічні ефекти: відправка даних на сервер, запис до бази даних, логування, analytics, навігація, показ системних діалогів. Без Mock ці взаємодії неможливо перевірити без запуску реальної інфраструктури. За даними Google Testing Blog, Mock — єдиний спосіб перевірити, що застосунок дійсно відправив analytics-подію, без підняття тестового сервера.

Mock та Stub: детальне порівняння

Різниця між Mock та Stub — одна з найбільш обговорюваних тем у тестуванні. Обидва типи замінюють реальну залежність, але принципово різними способами.

КритерійMockStub
Головне питанняЧи був викликаний метод?Який результат повернувся?
ВерифікаціяПоведінки (verify)Стану (assert)
Повернення данихОпціональноОбов'язково
Прикладverify(analytics).logEvent(“click”)assertEquals(5, repository.getCount())
Коли використовуватиПобічні ефектиПовернення даних

Практичне правило: Mock чи ні

Простий тест для вибору: задайте собі питання — «якщо я видалю цей рядок коду, тест впаде?». Якщо тест перевіряє значення, що повертається — потрібен Stub (перевірка через assert). Якщо тест перевіряє, що код викликав метод з правильними аргументами — потрібен Mock (перевірка через verify). Ця дихотомія випливає з патерну Command-Query Separation: методи, що змінюють стан (commands), потребують Mock; методи, що повертають дані (queries), потребують Stub.

Mockito та MockK: порівняння бібліотек

Вибір між Mockito та MockK — одне з перших рішень при налаштуванні тестового стеку Android-проєкту на Kotlin. Обидві бібліотеки виконують одне завдання, але з різним підходом до Kotlin-специфіки.

Mockito: перевірена класика

Mockito — де-факто стандарт для Java-проєктів. Версія 5.x підтримує mock-об'єкти для final-класів, static-методів та конструкторів завдяки вбудованому MockMaker. Для Kotlin-проєктів Mockito вимагає додаткового налаштування: mockito-kotlin розширення для покращеного синтаксису, mockito-inline для final-класів. Mockito не підтримує Kotlin-корутини та suspend-функції без додаткових адаптерів.

MockK: Kotlin-first підхід

MockK створений спеціально для Kotlin. Він нативно підтримує корутини (coEvery, coVerify), sealed class, data class, object-синглтони та extension-функції. Синтаксис MockK використовує DSL з лямбда-блоками, що виглядає природно в Kotlin-коді. MockK також вміє mock-ити властивості (property mocking) без додаткового налаштування — це важливо для Android-проєктів, що використовують LiveData, StateFlow та Delegates.

kotlin
// 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 секунд на повному прогоні тестів у великих проєктах.

Приклади Mock-тестів на Kotlin

Розглянемо три сценарії: тестування ViewModel з Mock-залежностями, тестування UseCase з перевіркою виклику API та тестування корутин з coVerify.

Приклад 1: ViewModel з Mock-аналітикою

kotlin
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") }
    }
}

Приклад 2: UseCase з асинхронною верифікацією

kotlin
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)
    }
}

Приклад 3: перевірка аргументів з ArgumentCaptor

kotlin
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 у мобільній розробці вимагає дотримання дисципліни. Порушення цих правил перетворює тести на крихку перешкоду, яка ламається при кожному рефакторингу.

Mock тільки зовнішні межі застосунку

Жорстке правило: Mock створюється тільки для залежностей, що перетинають межу застосунку: API-клієнти, бази даних, файлова система, системні сервіси (LocationManager, BluetoothAdapter, Camera). Внутрішні класи застосунку — domain-сутності, Value Object, прості утиліти — не повинні замінюватися на Mock. Їх поведінка тестується через реальні об'єкти.

Один assert / verify на тест

Кожен тест повинен містити рівно одну логічну перевірку — або verify (для Mock), або assert (для Stub). Не змішуйте перевірку стану та поведінки в одному тесті. Якщо потрібно перевірити і API-виклик, і результат — створіть два окремі тести з різними назвами. Це правило, відоме як «один assert на тест», сягає рекомендацій Kent Beck (2002).

  • Використовуйте relaxUnitFun = true в MockK для методів, що повертають Unit — інакше Mock кине виняток на неописаний виклик
  • Обмежуйте verify тільки критичними викликами — не перевіряйте кожен геттер і сеттер, це робить тести крихкими
  • Застосовуйте ArgumentMatchers осмислено — any() приховує важливі деталі, якщо аргумент критично важливий для бізнес-логіки
  • Не зловживайте verifyNoMoreInteractions — цей метод робить тест надмірно жорстким до будь-яких змін у production-коді
  • Використовуйте @MockkAnnotations для автоматичної ініціалізації Mock-об'єктів — це скорочує boilerplate та покращує читаність

Продвинуті техніки Mock-тестування

Крім базового mocking, існують продвинуті техніки, які вирішують специфічні завдання в мобільній розробці: тестування багатопотоковості, перевірка стану Flow та частковий mocking реальних об'єктів.

Partial Mock з spyK

Spy (або partial mock) дозволяє створити об'єкт, який делегує виклики реальної реалізації, але дозволяє перевизначити окремі методи. У MockK spyk створюється на основі реального екземпляра класу: val repo = spyk(InMemoryUserRepository()). Виклики, для яких задані очікування через every, йдуть через Mock; інші — через реальний об'єкт. Spy особливо корисний для тестування legacy-коду, де ін'єкція залежностей ще не впроваджена, і потрібно перевизначити тільки один метод.

Тестування StateFlow з Turbine

У сучасних Android-проєктах на Jetpack Compose ViewModel експонує стан через StateFlow. MockK дозволяє mock-ити Flow-залежності, а бібліотека Turbine спрощує перевірку емісії. Класичний патерн: MockK для UseCase, що повертає Flow, Turbine для перевірки емісії ViewModel. Цей стек рекомендований документацією Android Testing (Google, 2024) для проєктів на Kotlin Coroutines.

kotlin
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 відрізняється від Mockito?

Mock — це концепція, тип Test Double, що перевіряє поведінку. Mockito — це бібліотека для створення Mock-об'єктів у Java та Android. Інші бібліотеки: MockK (Kotlin), EasyMock (Java), Cuckoo (iOS).

Як Mock працює з Kotlin-корутинами?

Для тестування suspend-функцій з Mock використовуйте MockK (coEvery / coVerify) або Mockito з mockito-kotlin. MockK підтримує корутини нативно: coEvery визначає поведінку suspend-функції, coVerify перевіряє її виклик всередині корутини. Всі suspend-виклики повинні виконуватися всередині runTest (kotlinx-coroutines-test).

Чи може Mock повертати різні значення при повторних викликах?

Так. У MockK для цього використовується returnsMany: every { api.getData() } returnsMany listOf(response1, response2). У Mockito — ланцюжок thenReturn(value1).thenReturn(value2). Це корисно для тестування поведінки при послідовних викликах з різними відповідями.

Як очистити стан Mock між тестами?

У MockK використовуйте анотацію @MockK з полем relaxed = true та викликайте clearMocks(mock) у методі @After. У MockitoMockito.reset(mock). Найкраща практика: створювати новий Mock для кожного тесту через @Before, щоб виключити вплив між тестами.

Як Mock обробляє sealed class у Kotlin?

MockK коректно працює із sealed class: every { useCase() } returns Result.Success(data). Mockito не підтримує sealed class безпосередньо, вимагаючи обхідних шляхів. Це одна з причин, чому для Kotlin-проєктів рекомендується MockK замість Mockito.

Підсумки

  • Mock — тип Test Double, що перевіряє поведінку (verify), а не стан (assert) залежностей
  • Mockito — стандарт для Java/Android, MockK — Kotlin-first вибір з підтримкою корутин та sealed class
  • Головне правило: Mock для зовнішніх меж (мережа, БД, системні сервіси), реальні об'єкти для внутрішніх класів
  • Over-mocking — основний Anti-Pattern: надмірна заміна залежностей робить тести крихкими та малокорисними
  • Один тест — одна логічна перевірка: verify для Mock або assert для Stub, але не обидва в одному тесті
  • ArgumentCaptor / slot — правильний спосіб перевірки аргументів Mock-виклику замість сліпого any()
  • MockK рекомендується для Kotlin-проєктів: coEvery та coVerify нативно працюють з корутинами без додаткових адаптерів

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також