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