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 на тест

Каждый тест должен содержать ровно один logical check — либо 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: избыточная замена зависимостей делает тесты хрупкими и малополезными
  • Один тест — один logical check: verify для Mock или assert для Stub, но не оба в одном тесте
  • ArgumentCaptor / slot — правильный способ проверки аргументов Mock-вызова вместо слепого any()
  • MockK рекомендуется для Kotlin-проектов: coEvery и coVerify нативно работают с корутинами без дополнительных адаптеров

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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