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 е език със стойностна семантика, така че структурите 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 стек, работещ в паметта. Такъв 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 класовете в общ тестов модул (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. Това позволява симулиране на закъснения, грешки или проверка на междинни състояния. Такъв подход е полезен за тестване на състояния на зареждане на UI: fake прави пауза от 100 ms, а тестът проверява дали екранът показва лоудър. В продукционна среда 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 — това нарушава принципа на единната отговорност и обърква теста.
Тествайте fake със същите тестове като продукционната имплементация. Ако имате UserRepositoryTest, който проверява save, get, delete — пуснете го два пъти: с FakeUserRepository и с RealUserRepository. Това гарантира, че fake повтаря поведението на продукционния клас. Ако fake започне да се държи различно — тестът ще се провали и при двете имплементации.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също