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. Их поведение тестируется через реальные объекты.
Каждый тест должен содержать ровно один logical check — либо 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также