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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також