Test Doubles — это объекты-заменители, используемые в модульном тестировании вместо реальных зависимостей. Термин введён Gerard Meszaros в книге «xUnit Test Patterns» (2007) как обобщающее понятие для Mock, Stub, Fake, Spy и Dummy. По данным Martin Fowler (2024), Test Doubles позволяют изолировать тестируемый компонент от его окружения, делая тесты детерминированными, быстрыми и независимыми от внешних сервисов.
Главное
Test Doubles — это термин из автомобильной индустрии (каскадёры, «double» для актёров), перенесённый в разработку программного обеспечения. Как каскадёр заменяет актёра в опасной сцене, Test Double заменяет реальный компонент в тестовом сценарии. Это необходимо, когда реальная зависимость недоступна, медленна, недетерминирована или имеет побочные эффекты.
Понятие Test Double обобщает пять конкретных типов, каждый из которых решает свою задачу. Типология Meszaros является канонической и используется во всех современных руководствах по тестированию. Различие между типами — в степени контроля и верификации: от простого заполнения параметров (Dummy) до полной проверки последовательности вызовов (Mock).
Основная цель Test Doubles — изоляция тестируемого модуля. В мобильной разработке реальные зависимости — это API-серверы, базы данных, файловая система, сенсоры устройства, системные сервисы (LocationManager, Camera, Bluetooth). Использование этих компонентов напрямую делает тесты медленными, хрупкими и зависимыми от окружения. По данным Google Testing Blog (2023), хорошо изолированные unit-тесты выполняются за миллисекунды, а интеграционные — за секунды и минуты.
Классификация Gerard Meszaros включает пять типов Test Doubles, различающихся по поведению и цели использования. Понимание разницы между ними — основа грамотного модульного тестирования.
Dummy — это объект, который передаётся в тестируемый метод, но никогда не используется. Dummy нужен только для удовлетворения сигнатуры метода. В Kotlin это часто null, emptyList() или объект с заглушками. Dummy не должен содержать никакой логики — если он вызывается, тест должен упасть.
Fake — это упрощённая, но рабочая реализация интерфейса. В отличие от Mock и Stub, Fake содержит реальную бизнес-логику, но в упрощённом виде. Классический пример — InMemoryUserRepository, который хранит данные в HashMap вместо базы данных. Fake используется, когда нужно протестировать логику, зависящую от состояния, но без накладных расходов на реальную инфраструктуру.
| Тип | Назначение | Пример |
|---|---|---|
| Dummy | Заполнить параметр | null, пустой объект |
| Fake | Рабочая упрощённая реализация | InMemoryRepository |
| Stub | Вернуть фиксированное значение | when(api.getUser()).thenReturn(user) |
| Spy | Записать вызовы для проверки | verify(spy).save(user) |
| Mock | Проверить взаимодействие | verify(mock).sendEmail(email) |
Stub возвращает заранее заданные значения на определённые вызовы. Stub не проверяет, был ли он вызван, — он просто предоставляет данные. В Mockito Stub создаётся через when(method).thenReturn(value). Stub идеален для тестирования, когда нужно, чтобы зависимость вернула конкретное значение, но сам факт вызова не важен.
Spy — это обёртка вокруг реального объекта, которая записывает все вызовы для последующей верификации. В отличие от Mock, Spy делегирует вызовы реальному объекту, но позволяет проверить, что они произошли. В Mockito Spy создаётся через spy(realObject). Spy полезен для частичного mocking, когда хочется использовать реальный объект, но проверить некоторые вызовы.
Mock — это объект с предопределёнными ожиданиями вызовов. Mock проверяет, что определённые методы были вызваны с определёнными аргументами и в определённой последовательности. В отличие от Stub, Mock фокусируется на верификации поведения, а не на возврате данных. Mock — самый мощный и самый часто используемый тип Test Double в мобильной разработке.
Различие между Mock и Stub часто вызывает путаницу даже у опытных разработчиков. Основная разница — в цели: Stub проверяет состояние (state verification), Mock проверяет поведение (behavior verification).
Stub отвечает на вопрос: «вернул ли код правильный результат?». Mock отвечает на вопрос: «вызвал ли код правильные методы с правильными аргументами?». В мобильной разработке Stub используется, когда важен результат (например, данные из репозитория), а Mock — когда важны побочные эффекты (например, отправка email, запись в базу данных).
// Stub: проверка состояния
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: проверка поведения
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
Практические примеры всех пяти типов Test Doubles на Kotlin с использованием MockK — наиболее популярной библиотеки mocking для Android-проектов.
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub: возвращаем фиксированный ответ API
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: проверяем, что пользователь сохранён
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: Context не используется внутри Logger
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
Выбор типа Test Double зависит от того, что именно тестируется: состояние, поведение или интеграция. В мобильной разработке на Android и iOS сложились следующие рекомендации.
При тестировании ViewModel используйте Mock для зависимостей, которые производят побочные эффекты (репозитории, analytics, навигация), и Stub для зависимостей, которые возвращают данные (API-клиенты, ContentProvider). Это позволяет проверить, что ViewModel правильно обрабатывает как успешные, так и ошибочные сценарии.
На уровне Repository предпочтительны Fake (in-memory реализации базы данных) и Stub (фиксированные ответы API). Fake позволяет проверить логику кэширования и офлайн-режима без настройки SQLite. Stub имитирует различные HTTP-статусы: 200, 404, 500, timeout.
Неправильное использование Test Doubles — одна из самых частых причин хрупких тестов, которые ломаются при каждом рефакторинге.
Самая распространённая ошибка — mocking всего подряд. Если каждая зависимость в тесте заменена на Mock, тест перестаёт проверять реальное поведение. Mock должен быть только для внешних зависимостей (сеть, БД, файловая система, системные сервисы). Внутренние компоненты приложения (Value Object, data class, простые утилиты) не должны заменяться.
Вторая ошибка — создание Mock без определения ожиданий. Если метод вызывается без every / when, Mock возвращает значение по умолчанию (null, 0, false). Это может привести к ложно-положительным тестам, когда Mock молча возвращает null, а тест интерпретирует это как корректное поведение.
Третья ошибка — проверка каждого вызова каждого Mock. Verify должен использоваться только для вызовов, критически важных с точки зрения бизнес-логики. Избыточная верификация делает тесты хрупкими: изменение порядка вызовов в production-коде ломает тесты без изменения поведения.
Часто задаваемые вопросы
Stub возвращает данные и проверяет состояние (что вернулось), а Mock проверяет поведение (какие методы вызывались). Stub = «верни X», Mock = «проверь, что вызвали Y с аргументом Z». В реальных тестах один объект часто выступает и Stub, и Mock одновременно.
Fake предпочтительнее Mock, когда тестируется логика, зависящая от состояния: кэширование, офлайн-режим, транзакции. Fake (in-memory реализация) позволяет проверить эти сценарии без хрупких verify-вызовов. Mock лучше подходит для проверки отправки данных: analytics, push, email.
Для Android-проектов на Kotlin рекомендуется MockK. Она поддерживает корутины, suspend-функции, sealed class и extension-функции без дополнительных настроек. Для проектов на Java по-прежнему стандартом является Mockito — самая популярная библиотека с обширной документацией.
Для тестирования Kotlin Flow используйте библиотеку Turbine в паре с MockK. Turbine упрощает проверку эмиссии Flow: можно проверить порядок значений, завершение потока и исключения. Stub для Flow возвращает flowOf(value), Mock проверяет, что Flow был собран.
Да, но на уровне API-ответов, а не UI-компонентов. Библиотеки MockWebServer (OkHttp) и WireMock позволяют подменять HTTP-ответы в UI-тестах. Сами UI-компоненты (Compose, SwiftUI Views) не должны заменяться — их поведение тестируется через screenshot-тесты и Espresso.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также