Spy (mata-mata) — objek pengujian yang membungkus instance nyata dan mencatat informasi tentang setiap panggilan: metode apa yang dipanggil, dengan argumen apa, berapa kali. Berbeda dengan mock, spy menggunakan implementasi nyata dari objek yang dibungkus — panggilan melewati kode asli, dan spy hanya mencatat fakta. Setelah pengujian selesai, pengembang memeriksa catatan mata-mata: „Apakah metode sendAnalytics dipanggil tiga kali?”. Selengkapnya — di panduan pengujian Android.
Poin utama
Spy — adalah pembungkus di sekitar objek nyata yang mencegat semua panggilan metode dan mencatatnya. Logika nyata objek dijalankan: jika metode menyimpan data, menghitung nilai, atau membuat permintaan — semuanya terjadi seperti biasa. Selain itu, spy mencatat metadata: nama metode, argumen, jumlah panggilan, waktu eksekusi. Istilah ini termasuk dalam klasifikasi Meszaros (2007) dan dijelaskan secara rinci dalam artikel Martin Fowler „Mocks Aren't Stubs”.
Perbedaan utama dari Mock — mock sepenuhnya menggantikan objek dengan stub pengujian, semua metode secara default tidak melakukan apa pun. Spy membungkus objek yang ada: semua metode secara default bekerja seperti biasa, tetapi pada saat yang sama dicatat. Perbedaan ini mendasar: mock mengisolasi kode yang diuji dari kenyataan, spy mempertahankan kenyataan dan memungkinkan pengamatan terhadapnya. Pilihan di antara keduanya tergantung pada apa yang diuji.
Spy — pilihan yang tepat — jika kode yang diuji memodifikasi status objek nyata, dan pengujian harus memeriksa hasil (status) dan memastikan bahwa panggilan dilakukan dalam urutan yang benar. Mock tidak cocok karena tidak menjalankan implementasi nyata. Stub tidak cocok karena tidak mencatat panggilan. Spy adalah satu-satunya test double yang secara bersamaan mempertahankan logika nyata dan memberikan informasi tentang panggilan.
Mock — isolasi penuh. Jika pengujian tidak boleh bergantung pada implementasi objek nyata (misalnya, basis data atau klien jaringan), gunakan mock. Mock menjamin bahwa tidak ada panggilan yang mencapai komponen nyata. Ini aman dan dapat diprediksi. Kekurangan: mock tidak menjalankan logika nyata, jadi jika kode yang diuji bergantung pada nilai yang dikembalikan — harus dikonfigurasi secara eksplisit melalui when/stub.
Spy — logika nyata + observasi. Jika kode yang diuji berinteraksi dengan objek yang logikanya penting untuk pengujian, bukan hanya data — gunakan spy. Misalnya, AnalyticsTracker yang mengumpulkan peristiwa dan mengirimkannya secara berkala. Pengujian memeriksa apakah peristiwa ditambahkan ke buffer dan setelah pengiriman buffer dibersihkan. Mock tidak dapat memeriksa ini karena tidak menjalankan logika nyata tracker.
| Skenario | Spy | Mock |
|---|---|---|
| Logika nyata diperlukan | Ya | Tidak (stub) |
| Verifikasi panggilan | Ya (jumlah, argumen) | Ya (jumlah, argumen) |
| Stub parsial | Ya (beberapa metode — spy, lainnya — stub) | Tidak (semua metode — stub) |
| Risiko efek samping | Tinggi (kode nyata) | Nol |
| Kecepatan | Lebih rendah (logika nyata) | Lebih tinggi (stub) |
| Keterbacaan | Lebih rendah (lebih sulit dipahami mana yang nyata) | Lebih tinggi (semuanya eksplisit) |
Spy untuk segalanya — menggunakan spy alih-alih mock untuk semua pengujian adalah kesalahan. Spy menjalankan kode nyata yang dapat memiliki efek samping: menulis ke file, mengirim HTTP, mengubah status global. Jika modul yang diuji memanggil metode objek spy yang melakukan permintaan HTTP, pengujian menjadi pengujian integrasi, bukan pengujian unit. Aturan: jika spy membungkus objek dengan operasi I/O — ini bukan lagi pengujian unit. Gunakan mock untuk isolasi I/O, spy — hanya untuk objek in-memory tanpa efek eksternal.
Mockito.spy() — cara klasik membuat mata-mata di proyek Java/Kotlin. spy() menerima objek nyata dan mengembalikan pembungkus. Semua panggilan secara default didelegasikan ke objek nyata, dan hasilnya dicatat. Setelah pengujian selesai, melalui verify() dapat diperiksa jumlah panggilan dan argumen. Untuk metode yang harus mengembalikan data pengujian, digunakan doReturn/when — ini disebut „stub parsial” (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() — alternatif untuk proyek Kotlin dengan dukungan lebih baik untuk coroutine dan kelas sealed. MockK.spyk() membuat mata-mata, analog dengan Mockito.spy(). Mendukung coVerify untuk fungsi suspend dan every untuk stub parsial. Berbeda dengan Mockito, MockK tidak mendukung spy untuk kelas final (semua kelas di Kotlin bersifat final secara default) — kelas harus dibuka (open) atau menggunakan antarmuka.
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)
}
}
Stub parsial — teknik yang kuat tetapi berbahaya. Anda dapat membuat spy dari suatu objek dan menimpa (stub) hanya beberapa metode, membiarkan yang lainnya nyata. Contoh: spy-repository yang getUser()-nya mengembalikan data pengujian, dan saveUser()-nya benar-benar menyimpan ke daftar in-memory. Ini memungkinkan menggabungkan kelebihan stub (data terkontrol) dan spy (logika nyata). Kekurangan: kompleksitas membaca pengujian — tidak jelas metode mana yang nyata dan mana yang stub.
OCMock untuk Objective-C — pustaka yang mendukung pembuatan objek spy melalui niceMock. OCMock mencegat panggilan metode menggunakan runtime Objective-C dan mencatatnya. Setelah pengujian selesai, verify dipanggil. OCMock mendukung spy untuk objek apa pun (di Objective-C semua metode bersifat dinamis), yang memberikan keunggulan dibandingkan Swift, di mana spy hanya dimungkinkan melalui protokol.
// Membuat spy untuk objek nyata
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// Menjalankan pengujian
[spy trackEvent:@"login"];
// Verifikasi
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Swift protocol-based spy — di Swift tidak ada refleksi runtime Objective-C, sehingga spy dibuat secara manual. Struktur pengujian mengimplementasikan protokol dan secara internal memanggil objek nyata, sekaligus mencatat panggilan. Ini lebih banyak kode, tetapi sepenuhnya terkendali dan type-safe. Mata-mata manual tidak memerlukan pustaka eksternal dan tidak menggunakan runtime — semuanya diperiksa pada tahap kompilasi.
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)
}
}
Kapan menggunakan OCMock vs spy manual — untuk kode Objective-C gunakan OCMock (lebih sedikit boilerplate). Untuk Swift — mata-mata manual melalui protokol lebih disukai. Spy manual memberikan kendali penuh atas pencatatan panggilan, tidak memerlukan refleksi, dan bekerja dengan tipe nilai (struct). Satu-satunya kekurangan: kode kelas spy harus tetap sinkron dengan protokol saat menambahkan metode baru.
Memeriksa analytics — skenario paling umum penggunaan spy. Dalam kode produksi, panggilan analytics tersebar di seluruh aplikasi: login, logout, purchase, error. Pengujian membuat pembungkus spy untuk AnalyticsTracker, menjalankan skenario (login, melihat produk, menambahkan ke keranjang, membeli) dan memeriksa apakah semua peristiwa yang diperlukan telah dikirim dalam urutan yang benar. Mock tidak cocok karena AnalyticsTracker berisi logika buffering dan pengiriman.
Timer dan penjadwal — menguji kode yang menggunakan Handler (Android) atau Timer (iOS) sulit karena waktu nyata. Pembungkus spy untuk Scheduler mencatat tugas apa yang telah dijadwalkan dan dengan penundaan berapa. Pengujian membuat spy dari Handler nyata, menjalankan tindakan, dan memeriksa apakah Handler.postDelayed(runnable, delay) dipanggil dengan penundaan yang benar. Tugas nyata tidak dijalankan — spy mencegat dan mencatat panggilan.
Logging dan informasi debug — dalam produksi, log mungkin dinonaktifkan atau ditulis ke file. Pembungkus spy untuk Logger mencatat semua pesan ke daftar in-memory yang diperiksa pengujian setelah eksekusi. Ini memungkinkan memeriksa apakah pesan yang benar ditulis saat terjadi kesalahan, tanpa mengotori konsol. Mata-mata manual untuk Logger sangat berguna di iOS, karena OSLog tidak memiliki API pengujian.
Memeriksa urutan panggilan — beberapa skenario memerlukan urutan operasi yang ketat: buka koneksi, kirim data, tutup koneksi. Mockito memungkinkan memeriksa urutan melalui InOrder.verify(). Spy melakukan hal yang sama tetapi mempertahankan eksekusi nyata. Jika tidak hanya urutan tetapi juga hasil setiap langkah penting (koneksi benar-benar terbuka) — gunakan spy, bukan mock.
Pertanyaan yang Sering Diajukan
Spy membungkus objek nyata dan menjalankan logikanya, sekaligus mencatat panggilan. Mock sepenuhnya menggantikan objek dengan stub — tidak ada logika nyata yang dijalankan. Spy mempertahankan perilaku, mock tidak. Pilih spy ketika kerja nyata objek penting; pilih mock ketika pengujian perlu diisolasi dari ketergantungan eksternal.
Ketika pembungkus spy mengarah ke operasi I/O nyata. Jika spy membungkus objek yang menulis ke file, mengirim HTTP, atau membaca dari disk — pengujian berhenti menjadi pengujian unit. Kasus kedua: pengujian hanya memeriksa nilai yang dikembalikan tanpa minat pada panggilan — di sini stub sudah cukup, dan spy berlebihan. Kasus ketiga: kode bergantung pada status internal spy — ini adalah pengujian yang rapuh.
Ya, melalui spyk() — analog dengan Mockito.spy(). MockK.spyk() membuat mata-mata di sekitar objek nyata, mendukung every untuk stub parsial dan coVerify/coroutinesVerify untuk fungsi suspend. Keterbatasan: tidak bekerja dengan kelas final (perlu open atau antarmuka). Untuk kelas Java, MockK juga mendukung spyk(), tetapi memerlukan anotasi @MockKJvmInline.
Secara teknis — tidak. Mock adalah stub tanpa implementasi nyata. Spy menurut definisi membungkus objek nyata. Di Mockito, mock tidak dapat diubah menjadi spy. Tetapi sebaliknya bisa dilakukan: buat spy dan timpa sebagian metode melalui doReturn/when (partial mocking). Ini memberikan perilaku mirip mock untuk metode terpilih dari objek spy.
Wajib. Di Swift tidak ada proxy dinamis seperti di Java/Kotlin. Untuk membuat spy diperlukan protokol yang diimplementasikan oleh kelas produksi dan kelas spy. Swift-protocol-based spy adalah implementasi manual yang menerima objek nyata, mendelegasikan panggilan kepadanya, dan mencatat metadata. Alternatif: pustaka Cuckoo, yang menghasilkan kelas spy melalui SourceKit.
Ringkasan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga