Spy (шпионин) — тестов обект, който обвива реален екземпляр и записва информация за всяко извикване: кои методи са били извикани, с какви аргументи, колко пъти. За разлика от mock, spy използва реалната имплементация на обвития обект — извикванията преминават през истински код, а spy само записва фактите. След изпълнение на теста, разработчикът проверява записите на шпионина: „Дали методът sendAnalytics беше извикан три пъти?”. Повече — в ръководството за тестване на Android.
Основни точки
Spy — е обвивка около реален обект, която прихваща всички извиквания на методи и ги записва. Реалната логика на обекта се изпълнява: ако методът запазва данни, изчислява стойност или прави заявка — всичко се случва както обикновено. Допълнително, spy записва метаданни: име на метода, аргументи, брой извиквания, време на изпълнение. Терминът е част от класификацията на Meszaros (2007) и е подробно описан в статията на Martin Fowler „Mocks Aren't Stubs”.
Ключова разлика от Mock — mock напълно замества обекта с тестов заглушка, всички методи по подразбиране не правят нищо. Spy обвива съществуващ обект: всички методи по подразбиране работят както обикновено, но същевременно се записват. Тази разлика е принципна: mock изолира тествания код от реалността, spy запазва реалността и позволява наблюдение върху нея. Изборът между тях зависи от това какво се тества.
Spy — правилният избор — ако тестваният код модифицира състоянието на реален обект и тестът трябва едновременно да провери резултата (състоянието) и да се увери, че извикванията са направени в правилния ред. Mock не е подходящ, защото не изпълнява реалната имплементация. Stub не е подходящ, защото не записва извиквания. Spy е единственият test double, който едновременно запазва реалната логика и предоставя информация за извикванията.
Mock — пълна изолация. Ако тестът не трябва да зависи от имплементацията на реален обект (например база данни или мрежов клиент), използвайте mock. Mock гарантира, че нито едно извикване няма да достигне до реалния компонент. Това е безопасно и предвидимо. Недостатък: mock не изпълнява реална логика, така че ако тестваният код разчита на върнатата стойност — тя трябва да бъде изрично конфигурирана чрез when/stub.
Spy — реална логика + наблюдение. Ако тестваният код взаимодейства с обект, чиято логика е важна за теста, а не само данните — използвайте spy. Например AnalyticsTracker, който събира събития и периодично ги изпраща. Тестът проверява дали събитията са добавени в буфера и дали след изпращане буферът е изчистен. Mock не може да провери това, защото не изпълнява реалната логика на тракера.
| Сценарий | Spy | Mock |
|---|---|---|
| Нужна е реална логика | Да | Не (заглушка) |
| Верификация на извиквания | Да (брой, аргументи) | Да (брой, аргументи) |
| Частично стабване | Да (едни методи — spy, други — stub) | Не (всички методи — заглушки) |
| Риск от странични ефекти | Висок (реален код) | Нулев |
| Скорост | По-ниска (реална логика) | По-висока (заглушки) |
| Четимост | По-ниска (по-трудно за разбиране кое е реално) | По-висока (всичко е изрично) |
Spy за всичко — използването на spy вместо mock за всички тестове е грешка. Spy изпълнява реален код, който може да има странични ефекти: запис във файл, изпращане на HTTP, промяна на глобално състояние. Ако тестваният модул извиква метод на spy-обект, който прави HTTP заявка, тестът става интеграционен, а не единичен тест. Правило: ако spy обвива обект с I/O операции — това вече не е единичен тест. Използвайте mock за изолация на I/O, spy — само за in-memory обекти без външни ефекти.
Mockito.spy() — класическият начин за създаване на шпионин в Java/Kotlin проекти. spy() приема реален обект и връща обвивка. Всички извиквания по подразбиране се делегират на реалния обект, а резултатите се записват. След изпълнение на теста, чрез verify() може да се провери броя на извикванията и аргументите. За методи, които трябва да връщат тестови данни, се използва doReturn/when — това се нарича „частично стабване” (partial mocking).
class AnalyticsReporterTest {
private val realTracker = AnalyticsTracker()
private val spyTracker = Mockito.spy(realTracker)
fun test_event_tracked() {
val event = AnalyticsEvent("login")
spyTracker.track(event)
Mockito.verify(spyTracker).track(event)
assertEquals(1, spyTracker.getBufferedCount())
}
fun test_track_with_exception() {
Mockito.doThrow(RuntimeException("network"))
.when(spyTracker).flush()
spyTracker.track(AnalyticsEvent("login"))
assertTrue(spyTracker.hasPendingEvents())
}
}
MockK.spyk() — алтернатива за Kotlin проекти с по-добра поддръжка на корутини и sealed класове. MockK.spyk() създава шпионин, аналог на Mockito.spy(). Поддържа coVerify за suspend функции и every за частично стабване. За разлика от Mockito, MockK не поддържа spy за final класове (всички класове в Kotlin са final по подразбиране) — трябва или да отворите класа (open), или да използвате интерфейс.
class LoginUseCaseTest {
private val realRepo = UserRepository()
private val spyRepo = spyk(realRepo)
private val useCase = LoginUseCase(spyRepo)
fun test_login_calls_save() = runTest {
every { spyRepo.getUser(any()) } returns User("test")
val result = useCase.login("test", "pass")
coVerify { spyRepo.saveLoginTime(any()) }
assertTrue(result.isSuccess)
}
}
Частично стабване — мощна, но опасна техника. Можете да направите spy на обект и да презапишете (stub) само някои методи, оставяйки останалите реални. Пример: spy-репозиториум, чийто getUser() връща тестови данни, а saveUser() реално запазва в in-memory списък. Това позволява комбиниране на предимствата на stub-овете (контролирани данни) и spy (реална логика). Недостатък: сложност при четене на теста — не е очевидно кои методи са реални и кои са stub.
OCMock за Objective-C — библиотека, поддържаща създаване на spy-обекти чрез niceMock. OCMock прихваща извиквания на методи, използвайки Objective-C runtime, и ги записва. След изпълнение на теста се извиква verify. OCMock поддържа spy за всеки обект (в Objective-C всички методи са динамични), което дава предимство пред Swift, където spy е възможен само чрез протоколи.
// Създаване на spy за реален обект
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// Изпълнение на теста
[spy trackEvent:@"login"];
// Верификация
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Swift protocol-based spy — в Swift няма Objective-C runtime рефлексия, затова spy се създава ръчно. Тестовата структура имплементира протокола и вътрешно извиква реалния обект, едновременно записвайки извикванията. Това е повече код, но напълно контролируемо и типобезопасно. Ръчните шпиони не изискват външни библиотеки и не използват runtime — всичко се проверява на етапа на компилация.
protocol AnalyticsProtocol {
func trackEvent(name: String)
}
final class SpyAnalytics: AnalyticsProtocol {
private let real: AnalyticsProtocol
private var events: [String] = []
init(real: AnalyticsProtocol) {
self.real = real
}
func trackEvent(name: String) {
events.append(name)
real.trackEvent(name: name)
}
func verifyTracked(name: String) -> Bool {
return events.contains(name)
}
}
Кога да използвате OCMock срещу ръчен spy — за Objective-C код използвайте OCMock (по-малко boilerplate). За Swift — предпочитани са ръчните шпиони чрез протоколи. Ръчният spy дава пълен контрол върху записването на извиквания, не изисква рефлексия и работи с типове стойности (struct). Единственият недостатък: кодът на spy-класа трябва да се поддържа синхронизиран с протокола при добавяне на нови методи.
Проверка на аналитика — най-честият сценарий за използване на spy. В продукционния код, извикванията на аналитика са разпръснати из цялото приложение: login, logout, purchase, error. Тестът създава spy-обвивка за AnalyticsTracker, изпълнява сценария (вход, преглед на продукт, добавяне в кошницата, покупка) и проверява дали всички необходими събития са изпратени в правилния ред. Mock не е подходящ, защото AnalyticsTracker съдържа логика за буфериране и изпращане.
Таймери и планировчици — тестването на код, използващ Handler (Android) или Timer (iOS), е трудно поради реалното време. Spy-обвивка за Scheduler записва кои задачи са били планирани и с какво закъснение. Тестът създава spy на реалния Handler, изпълнява действието и проверява дали Handler.postDelayed(runnable, delay) е бил извикан с правилното закъснение. Реалната задача не се изпълнява — spy прихваща и записва извикването.
Логване и debug информация — в продукция, логовете може да са изключени или записвани във файл. Spy-обвивка за Logger записва всички съобщения в in-memory списък, който тестът проверява след изпълнение. Това позволява да се провери дали при грешка се записва правилно съобщение, без да се затрупва конзолата. Ръчните шпиони за Logger са особено полезни на iOS, където OSLog няма тестово API.
Проверка на последователността на извиквания — някои сценарии изискват строга последователност на операциите: отвори връзка, изпрати данни, затвори връзка. Mockito позволява проверка на последователността чрез InOrder.verify(). Spy прави същото, но запазва реалното изпълнение. Ако е важна не само последователността, но и резултатът от всяка стъпка (връзката реално се отвори) — използвайте spy, а не mock.
Често задавани въпроси
Spy обвива реален обект и изпълнява неговата логика, допълнително записвайки извиквания. Mock напълно замества обекта със заглушка — никаква реална логика не се изпълнява. Spy запазва поведението, mock — не. Изберете spy, когато реалната работа на обекта е важна; изберете mock, когато трябва да изолирате теста от външна зависимост.
Когато spy-обвивката води до реални I/O операции. Ако spy обвива обект, който записва във файл, изпраща HTTP или чете от диск — тестът престава да бъде единичен тест. Втори случай: тестът проверява само върнатата стойност без интерес към извиквания — тук stub е достатъчен, а spy е излишен. Трети: кодът разчита на вътрешното състояние на spy — това е крехък тест.
Да, чрез spyk() — аналог на Mockito.spy(). MockK.spyk() създава шпионин около реален обект, поддържа every за частично стабване и coVerify/coroutinesVerify за suspend функции. Ограничение: не работи с final класове (нужен е open или интерфейс). За Java класове, MockK също поддържа spyk(), но изисква анотация @MockKJvmInline.
Технически — не. Mock е заглушка, която не съдържа реална имплементация. Spy по дефиниция обвива реален обект. В Mockito не може да се превърне mock в spy. Но обратното е възможно: създайте spy и презапишете част от методите чрез doReturn/when (partial mocking). Това дава поведение, подобно на mock, за избрани методи на spy-обекта.
Задължително. В Swift няма динамично проксиране както в Java/Kotlin. За създаване на spy е необходим протокол, който се имплементира и от продукционния клас, и от spy класа. Swift-protocol-based spy е ръчна имплементация, която приема реален обект, делегира му извиквания и записва метаданни. Алтернатива: библиотека Cuckoo, която генерира spy класове чрез SourceKit.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също