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), стартиране. Тестът може последователно да постави няколко отговора в опашка за сценарии с пагинация или повторни опити.
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 поддръжка за корутини, разширяващи функции и 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 — библиотека за създаване на стабове и мокове в наследени iOS проекти. OCMock поддържа stub методи с аргументи и връщани стойности. Съвременните проекти в Swift предпочитат протокол-базиран подход с ръчни стабове — това дава контрол над всеки метод и не изисква външни зависимости. 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също