Stub (стаб, заглушка) — тестовый объект, возвращающий предопределённые ответы на вызовы методов вместо реальной реализации. В мобильной разработке стабы изолируют тестируемый модуль от сетевых запросов, базы данных и файловой системы, позволяя проверять логику без настройки окружения. В отличие от mock, stub не проверяет поведение — он только предоставляет данные. Подробнее — в статье Martin Fowler о test doubles.
Главное
Stub — это объект-заглушка, который заменяет реальную зависимость в тесте и возвращает заранее заданные значения на определённые вызовы. Термин введён в классификации Gerard Meszaros (2007) в книге «xUnit Test Patterns». Stub относится к категории test doubles — объектов, подменяющих реальные компоненты во время тестирования. Основная цель стаба — обеспечить тестируемый блок предсказуемыми данными, убрав неопределённость внешних систем.
Принцип работы — тест конфигурирует стаб перед выполнением: «когда метод getUsers() будет вызван, верни этот список пользователей». Stub не содержит бизнес-логики, не проверяет последовательность вызовов и не записывает историю обращений. Он просто стоит на месте реального компонента и выдаёт то, что ему сказали. В контексте Android-тестирования это означает: OkHttp-клиент не делает реальный запрос на сервер, а получает ответ из MockWebServer, сконфигурированного как stub.
Когда использовать — стабы оптимальны для тестирования UI-слоя (ViewModel, Presenter) и бизнес-логики (UseCase, Interactor), где нужно проверить реакцию на конкретные данные: список пуст, сервер вернул ошибку 500, истёк токен. Любой кейс, где тест требует определённого входного состояния — это задача для stub. На каждый тестовый сценарий создаётся своя конфигурация стаба, что делает тесты читаемыми и предсказуемыми.
Gerard Meszaros (2007) в книге «xUnit Test Patterns» выделил пять типов test doubles: dummy, stub, spy, mock, fake. Каждый тип решает свою задачу. Dummy — передаётся, но не используется. Stub — возвращает данные. Spy — записывает вызовы. Mock — проверяет поведение. Fake — содержит упрощённую логику. Понимание этой классификации помогает разработчику выбирать правильный инструмент для каждого тестового сценария.
Сетевые запросы — самый частый сценарий применения стабов. Приложение делает HTTP-вызовы к API, и в тесте нужно проверить реакцию на разные ответы: успешный JSON, ошибка 401 (неавторизован), таймаут, пустой массив. MockWebServer (OkHttp) на Android и URLProtocol (iOS) выступают стабами, возвращая предопределённые HTTP-ответы без реального соединения с сервером. Это ускоряет тесты с секунд до миллисекунд.
База данных — Room (Android) и CoreData (iOS) имеют in-memory варианты, но их настройка всё равно требует времени. Stub вместо репозитория возвращает заранее подготовленные списки Entity, не касаясь БД. Это особенно эффективно для тестирования ViewModel, где нужно проверить сортировку, фильтрацию или трансформацию данных. Тест выполняется за миллисекунды вне зависимости от объёма данных.
Системные сервисы — LocationManager, SensorManager, SharedPreferences требуют реального устройства или эмулятора. Stub для LocationProvider возвращает заданные координаты, для SensorManager — фиксированные значения акселерометра. На iOS аналог — CLLocationManager с тестовой реализацией делегата. Без стабов такие тесты требуют физического устройства с определёнными условиями.
Файловая система и кэш — загрузка изображений, кэширование ответов, работа с файлами конфигурации — все эти операции зависят от состояния диска. Stub для FileManager или ImageCache возвращает успех/ошибку без чтения реальных файлов. Это исключает ложные падения тестов из-за несовпадения путей или прав доступа на разных машинах разработчиков.
Разделение ответственности — три типа test doubles решают разные задачи. Stub: «дай мне данные». Mock: «проверь, что меня вызвали». Fake: «я работаю как настоящий, только проще». Разница критически важна для читаемости тестов: если тест использует mock там, где нужен stub, он перегружен verify-вызовами, не относящимися к проверяемому сценарию.
| Характеристика | Stub | Mock | Fake |
|---|---|---|---|
| Назначение | Предоставить данные | Проверить взаимодействие | Упрощённая реализация |
| Логика | Нет | Нет | Есть (но упрощённая) |
| Верификация | Нет | Да (verify) | Косвенная (через состояние) |
| Гибкость | Низкая — жёсткие ответы | Средняя | Высокая — логика адаптируется |
| Скорость | Максимальная | Высокая | Средняя |
| Пример | MockWebServer возвращает JSON | Mockito.verify(repository).save() | InMemoryRepository с HashMap |
Практическое правило — если тест проверяет, какие данные получил тестируемый компонент — используйте stub. Если тест проверяет, вызывал ли компонент метод зависимости с правильными аргументами — используйте mock. Если вы просто хотите заменить БД на хеш-таблицу — это fake. Смешивание типов в одном тесте делает его хрупким: при смене реализации придётся переписывать и stub, и verify-логику.
Stub с verify — распространённая ошибка, когда разработчик настраивает stub, а потом добавляет verify(stub).method(). Stub по определению не должен верифицироваться — для верификации есть mock. Если вам нужно проверить, что метод был вызван с конкретными аргументами, используйте Mockito.mock() вместо Mockito.stub(). Это разделение сохраняет намерение теста ясным для других разработчиков.
MockWebServer — библиотека OkHttp для создания HTTP-стабов на Android и JVM. Она поднимает локальный HTTP-сервер на указанном порту, который перехватывает запросы OkHttp-клиента и возвращает предопределённые ответы. Настройка занимает три строки: создать сервер, enqueue ответ, запустить. Тест может последовательно enqueue несколько ответов для сценариев с пагинацией или ретраями.
class UserRepositoryTest {
private val server = MockWebServer()
fun setup() {
server.start(8080)
val client = OkHttpClient.Builder()
.readTimeout(1, TimeUnit.SECONDS)
.build()
}
fun test_user_list_success() {
val json = "[{\"id\":1,\"name\":\"Alice\"}]"
server.enqueue(MockResponse()
.setBody(json)
.setResponseCode(200)
)
val result = repository.getUsers()
assertEquals(1, result.size)
}
fun teardown() {
server.shutdown()
}
}
MockK — альтернатива Mockito для Kotlin с first-class поддержкой корутин, extension-функций и sealed-классов. Стабы в MockK создаются через coEvery (для suspend-функций) и every (для обычных). В отличие от MockWebServer, MockK стабит отдельные методы-зависимости, а не HTTP-слой целиком. Это удобно для unit-тестов UseCase или Interactor, где зависимости — это абстракции репозиториев.
interface UserRepository {
suspend fun getUsers(): List<User>
}
class GetUsersUseCaseTest {
private val repo = mockk<UserRepository>()
private val useCase = GetUsersUseCase(repo)
fun test_empty_list() = runTest {
coEvery { repo.getUsers() } returns emptyList()
val result = useCase.invoke()
assertTrue(result.isEmpty())
coVerify(exactly = 1) { repo.getUsers() }
}
}
Best practice — для интеграционных тестов используйте MockWebServer (перехватывает реальный HTTP), для unit-тестов — MockK (стабит интерфейсы). Не стабите то, что не тестируете: если тест проверяет Repository, не стабите внутри него OkHttp-клиент — используйте реальный MockWebServer на уровне HTTP. Это правило сохраняет тесты релевантными и уменьшает хрупкость при рефакторинге.
Swift-протоколы как стабы — в iOS-native подходе stub реализуется через подстановку тестовой структуры, соответствующей протоколу зависимости. Вместо реального NetworkService тест получает StubNetworkService, который возвращает фиксированные данные. Swift — язык со статической типизацией, поэтому stub должен соответствовать тому же протоколу, что и реальный сервис. Компилятор гарантирует, что stub реализует все требуемые методы.
protocol NetworkServiceProtocol {
func fetchUsers() async throws -> [User]
}
struct StubNetworkService: NetworkServiceProtocol {
let result: Result<[User], Error>
func fetchUsers() async throws -> [User] {
try result.get()
}
}
final class UsersViewModelTests: XCTestCase {
func test_success_state() async {
let stub = StubNetworkService(
result: .success([User(name: "Alice")])
)
let vm = UsersViewModel(service: stub)
await vm.load()
XCTAssertEqual(vm.users.count, 1)
}
}
OCMock для Objective-C — библиотека для создания стабов и моков в legacy iOS-проектах. OCMock поддерживает stub-методы с аргументами и возвращаемыми значениями. Современные проекты на Swift предпочитают protocol-based подход с ручными стабами — это даёт контроль над каждым методом и не требует внешних зависимостей. OCMock остаётся опцией для проектов, где протоколизация всех зависимостей экономически нецелесообразна.
URLProtocol для HTTP-стабов — системный механизм iOS для перехвата сетевых запросов через подкласс URLProtocol. Тест регистрирует кастомный URLProtocol, который перехватывает URLSession и возвращает stub-ответы. Преимущество перед ручными стабами: не нужно менять архитектуру приложения — URLSession остаётся реальным, но данные подменяются на уровне протокола. Недостаток: сложнее дебажить, чем явный stub-сервис.
Часто задаваемые вопросы
Stub возвращает предопределённые данные и не проверяет факт вызова. Mock дополнительно верифицирует, что метод был вызван с правильными аргументами (verify). Stub отвечает на вопрос «что вернуть», Mock — на вопрос «был ли вызов». Используйте stub для проверки состояния, mock — для проверки взаимодействия.
Fake нужен, когда тесту требуется работающая (пусть и упрощённая) реализация — например, in-memory база данных вместо Room. Stub подходит для единичных сценариев с предопределёнными данными. Если вы повторяете один и тот же stub в 10 тестах — скорее всего, вам нужен Fake. Fake уменьшает дублирование, потому что логика живёт в одном классе.
На Android — MockK для Kotlin-объектов (object) поддерживает mockkObject(), включая статические методы Java-классов через mockkStatic(). На iOS — статические методы Swift не стабятся напрямую; используйте протоколы и DI, чтобы заменить вызов static на инстансный метод протокола. Статические стабы — технический долг, их следует избегать в новом коде.
Используйте MockWebServer (OkHttp) — он работает как локальный HTTP-сервер, enqueue-ющий ответы. Для Retrofit достаточно заменить базовый URL на localhost:8080. Для Ktor используйте MockEngine — встроенный механизм для подмены HttpStatement. Оба подхода работают без реального интернета и дают полный контроль над статус-кодом, телом и заголовками ответа.
Spy оборачивает реальный объект и записывает вызовы, а Stub полностью заменяет объект фиксированными ответами. Spy позволяет частично использовать реальную реализацию (остальные методы работают как есть), а stub — нет. Если вам нужно проверить, что метод был вызван, но при этом часть логики должна выполниться — используйте spy, а не stub.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также