Fake (фейк) — робоча спрощена реалізація залежності, яка поводиться як справжній компонент, але використовує in-memory сховище або інші легкі механізми замість продакшен-інфраструктури. На відміну від stub, fake містить реальну бізнес-логіку — сортування, фільтрацію, агрегацію — просто без зовнішніх ефектів. In-memory база даних замість Room або HashMap замість SharedPreferences — класичні приклади. Докладніше — в класифікації test doubles Мартіна Фаулера.
Головне
Fake — це повноцінна, але полегшена реалізація інтерфейсу, придатна для тестування. Термін введений Gerard Meszaros (2007) у книзі «xUnit Test Patterns.» На відміну від stub, який повертає жорстко задані відповіді, fake містить виконуваний код: він може сортувати список, фільтрувати за умовою, рахувати кількість записів. Єдина відмінність від продакшен-реалізації — fake працює з in-memory даними і не виконує реальних I/O-операцій.
Основна перевага — швидкість. Тести з fake виконуються за мілісекунди, тому що немає звернення до диска, мережі або бази даних. In-memory HashMap працює в 100—1000 разів швидше за Room або CoreData. При цьому fake перевіряє реальну бізнес-логіку: сортування, фільтрацію, агрегацію — все те, що stub не може перевірити, тому що stub повертає тільки те, що йому сказали. Fake дає впевненість, що код правильно обробляє дані, а не просто отримує попередньо визначену відповідь.
Fake кращий за Stub — якщо тестований компонент виконує кілька операцій над даними (дістав, відфільтрував, відсортував, зберіг), stub потребуватиме налаштування кожного виклику окремо. Fake містить логіку всередині себе — тест просто викликає методи і перевіряє результат. В IT Sectr ми використовуємо fake для всіх репозиторіїв в unit-тестах: fake-репозиторій із HashMap покриває 90% сценаріїв без налаштування Mockito або MockK.
Критерій вибору — визначте, що перевіряє тест: стан чи взаємодію. Якщо тест перевіряє стан (результат роботи) і використовує логіку — потрібен fake. Якщо тесту потрібні тільки вхідні дані без логіки — достатньо stub. Якщо тест перевіряє факт виклику методу — потрібен mock. Змішування типів test doubles в одному тесті ускладнює розуміння і збільшує крихкість.
| Критерій | Fake | Stub | Mock |
|---|---|---|---|
| Наявність логіки | Так (спрощена) | Ні | Ні |
| Швидкість | Висока | Максимальна | Висока |
| Перевірка поведінки | Непряма | Ні | Так (verify) |
| Обслуговування | Один клас на інтерфейс | Налаштування під кожен тест | Налаштування під кожен тест |
| Реалізм | Високий (код працює) | Низький (жорсткі дані) | Середній |
| Ризик хибних спрацьовувань | Низький | Середній | Високий (крихкі тести) |
Антипаттерн: Fake, який не fake — поширена помилка, коли розробник називає fake-об'єкт, який насправді є stub або mock. Якщо ваш InMemoryUserRepository не містить логіки (фільтрації, сортування) — це не fake, а stub з in-memory зберіганням. Fake відрізняється від stub саме наявністю виконуваної логіки. Якщо fake-репозиторій просто повертає те, що в нього поклали, і не обробляє дані — використовуйте mock або stub.
Практична рекомендація — починайте з fake для кожного репозиторію або сервісу. Якщо fake складніший за 50 рядків — розбийте на кілька класів. Якщо fake взагалі не потрібен (тест перевіряє тільки один сценарій із фіксованими даними) — використовуйте stub. Якщо тест перевіряє, що метод був викликаний із певними параметрами — mock. Не оптимізуйте вибір заздалегідь: напишіть fake, і якщо він виявиться надлишковим, замініть на stub у конкретному тесті.
Fake-репозиторій для Room — типовий приклад fake на Android. Продакшен-реалізація UserRepository використовує Room DAO із SQLite-запитами. Fake-версія зберігає дані в MutableList або HashMap і реалізує ті самі методи: getUser(id), saveUser(user), deleteUser(id). Fake містить логіку пошуку, фільтрації та сортування — таку ж, як у продакшен-репозиторії, але без SQL. Це дозволяє тестувати ViewModel і UseCase без налаштування Room-бази.
class FakeUserRepository : UserRepository {
private val users = mutableListOf<User>()
override suspend fun getUser(id: String): User? {
return users.find { it.id == id }
}
override suspend fun saveUser(user: User) {
val index = users.indexOfFirst { it.id == user.id }
if (index >= 0) users[index] = user
else users.add(user)
}
override suspend fun search(query: String): List<User> {
return users.filter {
it.name.contains(query, ignoreCase = true)
}
}
}
Fake для Retrofit API — замість MockWebServer (який stub, а не fake) можна створити реалізацію ApiService, яка повертає дані з in-memory колекції. Різниця: MockWebServer перехоплює HTTP і повертає JSON, а fake-ApiService працює на рівні Kotlin-інтерфейсу без серіалізації. Fake швидший (немає парсингу JSON) і простіший у налагодженні (працює в тому ж процесі, типізований). Підходить для тестів, де не важлива HTTP-семантика (статус-коди, заголовки).
Fake на Swift — будується через протоколи. Продакшен-клас реалізує протокол із реальною логікою (CoreData, URLSession). Fake-структура реалізує той самий протокол з in-memory сховищем і спрощеною логікою. Swift — мова з value-семантикою, тому fake-структури імутабельні та безпечні в багатопотокових тестах. Це дає перевагу перед Android-аналогами: не потрібно синхронізувати доступ до in-memory даних.
protocol UserRepositoryProtocol {
func getUser(id: String) async -> User?
func saveUser(user: User) async
}
final class FakeUserRepository: UserRepositoryProtocol {
private var storage: [String: User] = [:]
func getUser(id: String) async -> User? {
return storage[id]
}
func saveUser(user: User) async {
storage[user.id] = user
}
}
final class UserViewModelTests: XCTestCase {
func test_save_and_load() async {
let fake = FakeUserRepository()
let vm = UserViewModel(repository: fake)
let user = User(id: "1", name: "Alice")
await vm.saveUser(user)
let loaded = await vm.getUser(id: "1")
XCTAssertEqual(loaded?.name, "Alice")
}
}
Fake для CoreData — в iOS-проектах можна створити in-memory NSPersistentContainer, передавши description.type = NSInMemoryStoreType. Це повноцінний CoreData stack, але працюючий в пам'яті. Такий fake дозволяє тестувати NSFetchRequest, предикати та сортування без створення SQLite-файлу. Швидкість: тести на in-memory CoreData виконуються в 5—10 разів швидше, ніж на дисковому аналогу. Недолік: потрібно налаштовувати NSManagedObjectModel щоразу.
FakeURLProtocol — підклас URLProtocol для перехоплення мережевих запитів на iOS. Реєструється через URLProtocol.registerClass(fakeProtocol). Всередині містить in-memory словник URL -> Data і повертає дані без реального запиту. Відмінність від stub: FakeURLProtocol може перевіряти тіло запиту, заголовки та повертати різні відповіді залежно від вхідних даних. Це fake, тому що містить логіку маршрутизації запитів.
Fake як Test Fixture — виносьте fake-класи в загальний test-модуль (androidTest/sharedTest або TestSupport). Всі тести в проекті використовують один і той самий InMemoryUserRepository. Це усуває дублювання налаштування mock-об'єктів у кожному тесті та гарантує однакову поведінку. Зміна логіки fake оновлює всі тести одночасно. В IT Sectr ми зберігаємо fake-класи в sharedTest/java/com/itSectr/fake/ і підключаємо через implementation project(:sharedTest).
Fake з попередньо встановленими даними — часто тестам потрібен репозиторій, що вже містить деякі записи. Рішення: фабричний метод fakeWithData(vararg items) або вбудований метод addDefaultData(). Фабрика створює fake, наповнює його типовими даними та повертає готовий до використання об'єкт. Це зменшує boilerplate в тестах: замість налаштування mock-викликів тест просто викликає FakeUserRepository.withUsers(alice, bob).
Fake з підрахунком викликів — іноді потрібно перевірити не тільки стан, але й кількість звернень. Fake може містити лічильники: saveCallCount, getUserCallCount. Тест перевіряє лічильник після виконання. Це компроміс між чистим fake (перевірка стану) та mock (перевірка взаємодії). Лічильники не перевіряють аргументи та послідовність викликів — тільки кількість. Для перевірки аргументів використовуйте mock.
Fake з Callback — для тестування асинхронних сценаріїв fake може приймати callback при кожному виклику: beforeGetUser, afterSaveUser. Це дозволяє симулювати затримки, помилки або перевіряти проміжні стани. Такий підхід корисний для тестування loading-станів UI: fake робить паузу в 100 мс, а тест перевіряє, що екран показує лоадер. В продакшені callback відсутній — це чисто тестова функціональність.
Часті запитання
Fake містить робочу логіку — фільтрує, сортує, рахує. Stub повертає тільки заздалегідь задані відповіді без логіки. Якщо об'єкт має розгалуження (if/else, when) — це fake. Якщо він містить тільки return values — це stub. Fake дорожчий у підтримці, але дає більш реалістичні тести.
Коли fake-логіка не збігається з продакшен-логікою. Наприклад, FakeUserRepository використовує case-sensitive пошук, а продакшен — case-insensitive. Тест проходить, а в реальності баг. Рішення: тестуйте fake-логіку окремо або використовуйте fakes тільки для інтерфейсів із простою логікою (CRUD-операції). Для складної логіки пишіть інтеграційні тести з реальною БД.
In-memory database — один із варіантів fake. Room.inMemoryDatabaseBuilder() створює in-memory SQLite, який поводиться як продакшен-БД. Це повноцінний fake. Але fake може бути і на рівні репозиторію (без SQL), і на рівні мережі (FakeApiService). In-memory БД — окремий випадок fake, коли логіка максимально наближена до реальної.
Так, але з обережністю. Fake для репозиторію (дані), Mock для AnalyticsTracker (верифікація подій). Розділення по шарах: fake для шару даних, mock для шару аналітики/логіювання. Не робіть один об'єкт одночасно fake і mock — це порушує Single Responsibility Principle і заплутує тест.
Тестуйте fake тими самими тестами, що й продакшен-реалізацію. Якщо у вас є UserRepositoryTest, який перевіряє save, get, delete — запустіть його двічі: з FakeUserRepository і з RealUserRepository. Це гарантує, що fake повторює поведінку продакшен-класу. Якщо fake починає поводитися інакше — тест впаде на обох реалізаціях.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також