Mock — какво е това, 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 е задължителен, когато тестваният код взаимодейства с компоненти, които имат странични ефекти: изпращане на данни към сървър, запис в база данни, логване, аналитика, навигация, показване на системни диалози. Без 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 класове, статични методи и конструктори благодарение на вградения 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 проекти благодарение на директна работа с bytecode на 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). Вътрешните класове на приложението — домейн обекти, Value Object, прости инструменти — не трябва да бъдат заменяни с Mock. Тяхното поведение се тества чрез реални обекти.

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

Всеки тест трябва да съдържа точно една логическа проверка — или verify (за Mock), или assert (за Stub). Не смесвайте проверка на състояние и поведение в един тест. Ако трябва да се проверят както API извикването, така и резултатът — създайте два отделни теста с различни имена. Това правило, известно като «един assert на тест», произлиза от препоръките на Kent Beck (2002).

  • Използвайте relaxUnitFun = true в MockK за методи, връщащи Unit — иначе Mock ще хвърли изключение на неописано извикване
  • Ограничете verify само до критични извиквания — не проверявайте всеки getter и setter, това прави тестовете крехки
  • Прилагайте ArgumentMatchers смислено — any() скрива важни детайли, ако аргументът е критичен за бизнес логиката
  • Не злоупотребявайте с verifyNoMoreInteractions — този метод прави теста прекалено корав към каквито и да било промени в продукционния код
  • Използвайте @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 позволява mocking на 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 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също