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) мають ін-меморі варіанти, але їх налаштування все одно потребує часу. 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-клієнта та повертає попередньо визначені відповіді. Налаштування займає три рядки: створити сервер, додати відповідь у чергу, запустити його. Тест може послідовно додавати в чергу кілька відповідей для сценаріїв з пагінацією або повторними спробами.
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 з підтримкою першого класу для корутин, функцій розширення та sealed-класів. Стаби в MockK створюються через coEvery (для suspend-функцій) та every (для звичайних). На відміну від MockWebServer, MockK стабить окремі методи-залежності, а не весь HTTP-шар. Це зручно для модульних тестів 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), для модульних тестів — MockK (стабить інтерфейси). Не стабте те, що ви не тестуєте: якщо тест перевіряє Repository, не стабте OkHttp-клієнт всередині нього — використовуйте справжній MockWebServer на рівні HTTP. Це правило зберігає тести релевантними та зменшує хрупкість під час рефакторингу.
Swift-протоколи як стаби — в iOS-нативному підході 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 — бібліотека для створення стабів та моків у застарілих iOS-проектах. OCMock підтримує stub-методи з аргументами та поверненими значеннями. Сучасні проекти на Swift надають перевагу підходу на основі протоколів з ручними стабами — це дає контроль над кожним методом і не потребує зовнішніх залежностей. OCMock залишається опцією для проектів, де протоколізація всіх залежностей економічно недоцільна.
URLProtocol для HTTP-стабів — системний механізм iOS для перехоплення мережевих запитів через підклас URLProtocol. Тест реєструє кастомний URLProtocol, який перехоплює URLSession та повертає stub-відповіді. Перевага перед ручними стабами: не потрібно змінювати архітектуру застосунка — URLSession залишається справжнім, але дані підмінюються на рівні протоколу. Недолік: складніше налагоджувати, ніж явний stub-сервіс.
Часто запитують
Stub повертає попередньо визначені дані та не перевіряє факт виклику. Mock додатково верифікує, що метод був викликаний з правильними аргументами (verify). Stub відповідає на питання «що повернути», Mock — на питання «чи був виклик». Використовуйте stub для перевірки стану, mock — для перевірки взаємодії.
Fake потрібен, коли тесту потрібна працююча (навіть спрощена) реалізація — наприклад, ін-меморі база даних замість Room. Stub підходить для одиничних сценаріїв з попередньо визначеними даними. Якщо ви повторюєте одного й того ж stuba в 10 тестах — наймовірніше, вам потрібен Fake. Fake зменшує дублювання, оскільки логіка живе в одному класі.
На Android — MockK для Kotlin-об’єктів (object) підтримує mockkObject(), включаючи статичні методи Java-класів через mockkStatic(). На iOS — статичні методи Swift не стабяться безпосередньо; використовуйте протоколи та DI, щоб замінити static-виклик на інстансний метод протоколу. Статичні стаби — технічний борг, їх слід уникати в новому коді.
Використовуйте MockWebServer (OkHttp) — він працює як локальний HTTP-сервер, що додає відповіді у чергу. Для Retrofit достатьньо замінити базову URL на localhost:8080. Для Ktor використовуйте MockEngine — вбудований механізм для підміни HttpStatement. Обидва підходи працюють без реального інтернету та дають повний контроль над кодом статусу, тілом та заголовками відповіді.
Spy обгортає реальний об’єкт та записує виклики, а Stub повністю замінює об’єкт фіксованими відповідями. Spy дозволяє частково використовувати реальну реалізацію (інші методи працюють як є), а stub — ні. Якщо вам потрібно перевірити, що метод був викликаний, але частина логіки має виконатися — використовуйте spy, а не stub.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також