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-запит, тест стає інтеграційним, а не unit-тестом. Правило: якщо spy обгортає об'єкт з I/O-операціями — це вже не unit-тест. Використовуйте 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), або використовувати interface.
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 немає runtime-рефлексії Objective-C, тому 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 vs ручний spy — для Objective-C-коду використовуйте OCMock (менше boilerplate). Для Swift — переважні ручні шпигуни через протоколи. Ручний spy дає повний контроль над записом викликів, не потребує reflection і працює з value-типами (struct). Єдиний недолік — потрібно підтримувати код spy-класу синхронізованим з протоколом при додаванні нових методів.
Перевірка аналітики — найчастіший сценарій використання spy. У production-коді виклики аналітики розкидані по всьому додатку: login, logout, purchase, error. Тест створює spy-обгортку для AnalyticsTracker, виконує сценарій (логін, перегляд товару, додавання в кошик, покупка) і перевіряє, що всі потрібні події були надіслані в правильному порядку. Mock не підходить, тому що AnalyticsTracker містить логіку буферизації та надсилання.
Таймери та планувальники — тестування коду, що використовує Handler (Android) або Timer (iOS), складне через реальний час. Spy-обгортка для Scheduler фіксує, які завдання були заплановані та з якою затримкою. Тест створює spy реального Handler, виконує дію та перевіряє, що Handler.postDelayed(runnable, delay) був викликаний з правильною затримкою. Реальне завдання при цьому не виконується — spy перехоплює та записує виклик.
Логування та дебаг-інформація — у production логи можуть бути вимкнені або писати у файл. 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 або читає з диска — тест перестає бути unit-тестом. Другий випадок: тест перевіряє лише значення, що повертається, без інтересу до викликів — тут достатньо stub, а spy надлишковий. Третій: код покладається на внутрішній стан spy — це крихкий тест.
Так, через spyk() — аналог Mockito.spy(). MockK.spyk() створює шпигуна навколо реального об'єкта, підтримує every для часткового стаблення та coVerify/coroutinesVerify для suspend-функцій. Обмеження: не працює з final-класами (потрібен open або interface). Для 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також