Spy (جاسوس) — یک شیء آزمایشی است که یک نمونه واقعی را میپیچد و اطلاعات مربوط به هر فراخوانی را ثبت میکند: کدام متدها فراخوانی شدند، با چه آرگومانهایی، چند بار. بر خلاف mock، spy از پیادهسازی واقعی شیء پیچیدهشده استفاده میکند — فراخوانیها از کد واقعی عبور میکنند و spy فقط حقایق را ثبت میکند. پس از اجرای تست، توسعهدهنده records جاسوس را بررسی میکند: «آیا متد sendAnalytics سه بار فراخوانی شد؟». بیشتر — در راهنمای تست اندروید.
نکات اصلی
Spy — یک پوشش دور شیء واقعی است که تمام فراخوانیهای متدها را رهگیری و ثبت میکند. منطق واقعی شیء اجرا میشود: اگر متد دادهها را ذخیره میکند، مقدار را محاسبه میکند یا درخواست میدهد — همه چیز طبق معمول انجام میشود. علاوه بر این، spy فراداده را ثبت میکند: نام متد، آرگومانها، تعداد فراخوانیها، زمان اجرا. این اصطلاح در طبقهبندی Meszaros (2007) قرار دارد و در مقاله Martin Fowler «Mocks Aren't Stubs» به تفصیل شرح داده شده است.
تفاوت کلیدی با Mock — mock به طور کامل شیء را با یک stub آزمایشی جایگزین میکند، همه متدها به طور پیشفرض هیچ کاری انجام نمیدهند. Spy شیء موجود را میپوشاند: همه متدها به طور پیشفرض مثل همیشه کار میکنند، اما در عین حال ثبت میشوند. این تفاوت اساسی است: mock کد آزمایشی را از واقعیت جدا میکند، spy واقعیت را حفظ میکند و امکان مشاهده آن را فراهم میکند. انتخاب بین آنها بستگی به این دارد که چه چیزی آزمایش میشود.
Spy — انتخاب درست — اگر کد آزمایشی وضعیت شیء واقعی را تغییر میدهد و تست باید هم نتیجه (وضعیت) را بررسی کند و هم مطمئن شود که فراخوانیها به ترتیب درست انجام شدهاند. Mock مناسب نیست زیرا پیادهسازی واقعی را اجرا نمیکند. Stub مناسب نیست زیرا فراخوانیها را ثبت نمیکند. Spy تنها test double است که همزمان هم منطق واقعی را حفظ میکند و هم اطلاعاتی درباره فراخوانیها ارائه میدهد.
Mock — ایزولاسیون کامل. اگر تست نباید به پیادهسازی شیء واقعی وابسته باشد (مثلاً پایگاه داده یا کلاینت شبکه)، از mock استفاده کنید. Mock تضمین میکند که هیچ فراخوانی به مؤلفه واقعی نخواهد رسید. این ایمن و قابل پیشبینی است. نکته منفی: mock منطق واقعی را اجرا نمیکند، بنابراین اگر کد آزمایشی به مقدار بازگشتی وابسته است — باید آن را به صراحت از طریق when/stub پیکربندی کرد.
Spy — منطق واقعی + مشاهده. اگر کد آزمایشی با شیئی تعامل دارد که منطق آن برای تست مهم است، نه فقط دادهها — از spy استفاده کنید. مثلاً AnalyticsTracker که رویدادها را جمعآوری کرده و به صورت دورهای ارسال میکند. تست بررسی میکند که رویدادها به بافر اضافه شدهاند و پس از ارسال، بافر پاک شده است. Mock نمیتواند این را بررسی کند زیرا منطق واقعی tracker را اجرا نمیکند.
| سناریو | Spy | Mock |
|---|---|---|
| منطق واقعی لازم است | بله | خیر (stub) |
| تأیید فراخوانیها | بله (تعداد، آرگومانها) | بله (تعداد، آرگومانها) |
| Stub جزئی | بله (برخی متدها — spy، بقیه — stub) | خیر (همه متدها — stub) |
| ریسک عوارض جانبی | بالا (کد واقعی) | صفر |
| سرعت | کمتر (منطق واقعی) | بیشتر (stub) |
| خوانایی | کمتر (درک اینکه چه چیزی واقعی است دشوارتر) | بیشتر (همه چیز صریح) |
Spy برای همه چیز — استفاده از spy به جای mock برای همه تستها اشتباه است. Spy کد واقعی را اجرا میکند که ممکن است عوارض جانبی داشته باشد: نوشتن در فایل، ارسال HTTP، تغییر وضعیت سراسری. اگر ماژول آزمایشی متدی از شیء spy را فراخوانی کند که درخواست HTTP میدهد، تست تبدیل به تست یکپارچهسازی میشود، نه unit-test. قانون: اگر spy شیءای با عملیات I/O را میپوشاند — این دیگر unit-test نیست. از mock برای ایزوله کردن I/O استفاده کنید، از spy — فقط برای اشیاء درون حافظهای بدون اثرات خارجی.
Mockito.spy() — روش کلاسیک ایجاد جاسوس در پروژههای Java/Kotlin. spy() یک شیء واقعی دریافت میکند و یک پوشش برمیگرداند. همه فراخوانیها به طور پیشفرض به شیء واقعی واگذار میشوند و نتایج ثبت میشوند. پس از اجرای تست از طریق verify() میتوان تعداد فراخوانیها و آرگومانها را بررسی کرد. برای متدهایی که باید داده تستی برگردانند از doReturn/when استفاده میشود — به این «stub جزئی» (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 برای stub جزئی پشتیبانی میکند. بر خلاف 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)
}
}
Stub جزئی — تکنیک قدرتمند اما خطرناک. شما میتوانید spy یک شیء ایجاد کرده و فقط برخی متدها را بازنویسی (stub) کنید و بقیه را واقعی بگذارید. مثال: یک spy-repository که getUser() داده تستی برمیگرداند و saveUser() واقعاً در لیست درون حافظه ذخیره میکند. این امکان ترکیب مزایای stubها (داده کنترلشده) و spy (منطق واقعی) را فراهم میکند. نکته منفی: پیچیدگی خواندن تست — مشخص نیست کدام متدها واقعی و کدام stub هستند.
OCMock برای Objective-C — کتابخانهای که از ایجاد اشیاء spy از طریق niceMock پشتیبانی میکند. OCMock فراخوانی متدها را با استفاده از runtime Objective-C رهگیری و ثبت میکند. پس از اجرای تست، 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 به صورت دستی ایجاد میشود. ساختار تست پروتکل را پیادهسازی میکند و در داخل شیء واقعی را فراخوانی میکند، همزمان فراخوانیها را ثبت میکند. این کد بیشتری است، اما کاملاً قابل کنترل و type-safe است. جاسوسهای دستی نیاز به کتابخانههای خارجی ندارند و از 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 دستی کنترل کامل بر ثبت فراخوانیها میدهد، نیاز به reflection ندارد و با value-types (struct) کار میکند. تنها نکته منفی: نیاز به نگهداری کد کلاس spy همگام با پروتکل هنگام افزودن متدهای جدید.
بررسی analytics — رایجترین سناریوی استفاده از spy. در کد تولیدی، فراخوانیهای analytics در سراسر برنامه پراکنده هستند: ورود، خروج، خرید، خطا. تست یک پوشش spy برای AnalyticsTracker ایجاد میکند، سناریو را اجرا میکند (ورود، مشاهده محصول، افزودن به سبد خرید، خرید) و بررسی میکند که همه رویدادهای لازم به ترتیب درست ارسال شدهاند. Mock مناسب نیست زیرا AnalyticsTracker شامل منطق بافرینگ و ارسال است.
تایمرها و زمانبندها — تست کدی که از Handler (Android) یا Timer (iOS) استفاده میکند به دلیل زمان واقعی دشوار است. پوشش spy برای Scheduler ثبت میکند که چه کارهایی و با چه تأخیری برنامهریزی شدهاند. تست spy واقعی Handler را ایجاد میکند، عمل را اجرا میکند و بررسی میکند که Handler.postDelayed(runnable, delay) با تأخیر صحیح فراخوانی شده است. کار واقعی در این زمان اجرا نمیشود — spy فراخوانی را رهگیری و ثبت میکند.
لاگگیری و اطلاعات اشکالزدایی — در تولید، لاگها ممکن است خاموش یا در فایل نوشته شوند. پوشش spy برای Logger همه پیامها را در یک لیست درون حافظه ثبت میکند که تست پس از اجرا بررسی میکند. این امکان بررسی نوشته شدن پیام صحیح هنگام خطا را بدون شلوغ کردن کنسول فراهم میکند. جاسوسهای دستی برای Logger به ویژه در iOS مفید هستند، زیرا OSLog API تستی ندارد.
بررسی ترتیب فراخوانیها — برخی سناریوها نیاز به ترتیب دقیق عملیات دارند: باز کردن اتصال، ارسال داده، بستن اتصال. Mockito اجازه بررسی ترتیب را از طریق InOrder.verify() میدهد. Spy همین کار را انجام میدهد اما اجرای واقعی را حفظ میکند. اگر نه تنها ترتیب، بلکه نتیجه هر مرحله نیز مهم است (اتصال واقعاً باز شد) — از spy استفاده کنید، نه mock.
سؤالات متداول
Spy شیء واقعی را میپوشاند و منطق آن را اجرا میکند، علاوه بر این فراخوانیها را ثبت میکند. Mock به طور کامل شیء را با stub جایگزین میکند — هیچ منطق واقعی اجرا نمیشود. Spy رفتار را حفظ میکند، mock نه. وقتی کار واقعی شیء مهم است spy را انتخاب کنید؛ وقتی باید تست را از وابستگی خارجی جدا کنید mock را انتخاب کنید.
وقتی پوشش spy منجر به عملیات واقعی I/O میشود. اگر spy شیءای را میپوشاند که در فایل مینویسد، HTTP ارسال میکند یا از دیسک میخواند — تست دیگر unit-test نیست. مورد دوم: تست فقط مقدار بازگشتی را بدون علاقه به فراخوانیها بررسی میکند — اینجا stub کافی است و spy اضافی است. مورد سوم: کد به وضعیت داخلی spy متکی است — این یک تست شکننده است.
بله، از طریق spyk() — مشابه Mockito.spy(). MockK.spyk() یک جاسوس دور شیء واقعی ایجاد میکند، از every برای stub جزئی و coVerify/coroutinesVerify برای توابع suspend پشتیبانی میکند. محدودیت: با کلاسهای final کار نمیکند (نیاز به open یا interface). برای کلاسهای Java، MockK نیز از spyk() پشتیبانی میکند اما نیاز به annotation @MockKJvmInline دارد.
از نظر فنی — خیر. Mock یک stub بدون پیادهسازی واقعی است. 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 از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید