Spy (جاسوس) — كائن اختباري يغلف مثالاً حقيقياً ويسجل معلومات عن كل استدعاء: أي الدوال تم استدعاؤها، بما ذه من الوسائط، وكم مرة. على عكس المحاكاة (mock)، يستخدم المتجسس التنفيذ الحقيقي للكائن المغلف — تمر الاستدعاءات عبر الكود الحقيقي، ويقوم المتجسس فقط بتسجيل الحقائق. بعد تنفيذ الاختبار، يتحقق المطور من سجلات الجاسوس: «هل تم استدعاء الدالة sendAnalytics ثلاث مرات؟». اعرف المزيد في دليل اختبارات Android.
النقاط الرئيسية
Spy — غلاف حول كائن حقيقي يعترض جميع استدعاءات الدوال ويسجلها. يتم تنفيذ المنطق الحقيقي للكائن: إذا كانت الدالة تحفظ البيانات، أو تحسب قيمة، أو تقوم بطلب — كل شيء يحدث كالعادة. بالإضافة، يسجل المتجسس البيانات الوصفية: اسم الدالة، الوسائط، عدد الاستدعاءات، وقت التنفيذ. المصطلح هو جزء من تصنيف Meszaros (2007) ويم وصفه بالتفصيل في مقال مارتن فاولر «Mocks Aren’t Stubs».
الفرق الرئيسي عن Mock — يستبدل mock الكائن بكامله بكابئ اختباري؛ جميع الدوال لا تفعل شيئاً افتراضياً. يغلف spy كائناً موجوداً: جميع الدوال تعمل كالعادة افتراضياً، ولكن يتم تسجيلها أيضاً. هذا التمايز أساسي: mock يعزل الكود المختبر عن الواقع، بينما spy يحافظ على الواقع ويسمح بمراقبته. يعتمد الاختيار بينهما على ما يتم اختباره.
Spy هو الخيار الصحيح — إذا كان الكود المختبر يعدل حالة كائن حقيقي، ويجب على الاختبار تحقق كلاً من النتيجة (الحالة) والتأكد من أن الاستدعاءات تمت بالترتيب الصحيح. mock ليس مناسباً لأنه لا ينفذ التنفيذ الحقيقي. stub ليس مناسباً لأنه لا يسجل الاستدعاءات. Spy هو بديل الاختبار الوحيد الذي يحافظ على المنطق الحقيقي ويوفر معلومات عن الاستدعاءات.
Mock — عزل كامل. إذا كان الاختبار لا يجب أن يعتمد على تنفيذ الكائن الحقيقي (على سبيل المثال، قاعدة بيانات أو عميل شبكة)، استخدم mock. mock يضمن عدم وصول أي استدعاء إلى المكون الحقيقي. هذا آمن وقابل للتنبؤ. العيب: mock لا ينفذ منطقاً حقيقياً، لذا إذا كان الكود المختبر يعتمد على قيمة عائدة، فيجب تكوينه صراحةً عبر when/stub.
Spy — منطق حقيقي + مراقبة. إذا كان الكود المختبر يتفاعل مع كائن منطقه مهم للاختبار، وليس مجرد بيانات — استخدم spy. على سبيل المثال، AnalyticsTracker الذي يجمع الأحداث ويرسلها دورياً. يتحقق الاختبار من أن الأحداث أضيفت إلى المخزن المؤقت، وبعد الإرسال تم مسح المخزن المؤقت. mock لا يستطيع التحقق من ذلك لأنه لا ينفذ المنطق الحقيقي للمتتبع.
| السيناريو | Spy | Mock |
|---|---|---|
| منطق حقيقي مطلوب | نعم | لا (كابئ) |
| التحقق من الاستدعاءات | نعم (العدد، الوسائط) | نعم (العدد، الوسائط) |
| الكبس الجزئي | نعم (بعض الدوال spy، الأخرى stub) | لا (جميع الدوال كوابئ) |
| خطر الآثار الجانبية | عالٍ (كود حقيقي) | معدوم |
| السرعة | أقل (منطق حقيقي) | أكبر (كوابئ) |
| قابلية القراءة | أقل (أصعب فهم ما هو حقيقي) | أكبر (كل شيء صريح) |
Spy لكل شيء — استخدام spy بدلاً من mock لجميع الاختبارات هو خطأ. ينفذ spy كوداً حقيقياً قد يكون له آثار جانبية: الكتابة إلى ملف، إرسال HTTP، تغيير الحالة العامة. إذا كان المودول المختبر يستدعي دالة من كائن spy يقوم بطلب HTTP، يصبح الاختبار اختبار تكامل، ليس اختبار وحدة. القاعدة: إذا كان spy يغلف كائناً بعمليات إدخال/إخراج — فهو ليس اختبار وحدة. استخدم mocks لعزل الإدخال/الإخراج، و spies فقط للكائنات داخل الذاكرة دون تأثيرات خارجية.
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 classes). ينشئ MockK.spyk() متجسساً، مماثلاً لـ Mockito.spy(). يدعم coVerify للدوال suspend و every للكبس الجزئي. على عكس Mockito، لا يدعم MockK المتجسسين للفئات 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() فعلياً في قائمة داخل الذاكرة. يتيح هذا الجمع بين مزايا الكوابئ (بيانات متحكم فيها) والمتجسسين (منطق حقيقي). العيب: قابلية القراءة للاختبار تتضرر — ليس من الواضح أي الدوال حقيقية وأيها كوابئ.
OCMock لـ Objective-C — مكتبة تدعم إنشاء كائنات spy عبر niceMock. تعترض OCMock استدعاءات الدوال باستخدام وقت تنفيذ Objective-C وتسجلها. بعد الاختبار، يتم استدعاء verify. تدعم OCMock المتجسسين لأي كائن (جميع الدوال في Objective-C ديناميكية)، مما يمنحها ميزة على Swift، حيث المتجسسون ممكنون فقط عبر البروتوكولات.
// إنشاء spy لكائن حقيقي
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// تنفيذ الاختبار
[spy trackEvent:@"login"];
// التحقق
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Spy مبني على البروتوكول في Swift — ليس في Swift انعكاس وقت التنفيذ كما في Objective-C، لذا يتم إنشاء المتجسسين يدوياً. يقوم هيكل اختباري بتنفيذ بروتوكول ويستدعي داخلياً الكائن الحقيقي مع تسجيل الاستدعاءات في نفس الوقت. يتطلب هذا مزيداً من الكود، ولكنه قابل للتحكم بالكامل وآمن من حيث الأنواع. لا تتطلب المتجسسون اليدويون مكتبات خارجية ولا يستخدمون وقت التنفيذ — كل شيء يتم التحقق منه عند الترجمة.
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 يدوي تحكماً كاملاً في تسجيل الاستدعاءات، ولا يتطلب انعكاساً ويعمل مع أنواع القيم (structs). العيب الوحيد: تحتاج إلى الحفاظ على متطابقة كود فئة spy مع البروتوكول عند إضافة دوال جديدة.
التحقق من التحليلات — أكثر حالات استخدام المتجسسين شيوعاً. في كود الإنتاج، تكون استدعاءات التحليل منتشرة في جميع أنحاء التطبيق: تسجيل الدخول، تسجيل الخروج، الشراء، الخطأ. ينشئ الاختبار غلافًاً 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 يستبدل الكائن بكامله بكابئ — لا يتم تنفيذ أي منطق حقيقي. spy يحافظ على السلوك، mock لا. اختر spy عندما يكون العمل الحقيقي للكائن مهماً؛ اختر mock عندما تحتاج إلى عزل الاختبار عن الاعتماد الخارجي.
عندما يؤدي غلاف spy إلى عمليات إدخال/إخراج حقيقية. إذا كان spy يغلف كائناً يكتب في ملف، أو يرسل HTTP، أو يقرأ من القرص — فالاختبار ليس اختبار وحدة. الحالة الثانية: الاختبار يتحقق فقط من قيمة العائد دون الاهتمام بالاستدعاءات — هنا يكفي 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). يعطي هذا سلوكاً مشابهاً للمحاكاة لدوال مختارة من كائن spy.
نعم، يتطلب. في Swift، لا يوجد وكيل ديناميكي كما في Java/Kotlin. لإنشاء spy، تحتاج إلى بروتوكول تقوم بتنفيذه كلاً من فئة الإنتاج وفئة spy. spy المبني على البروتوكول في Swift هو تنفيذ يدوي يأخذ كائناً حقيقياً، يفوض له الاستدعاءات ويسجل البيانات الوصفية. البديل: مكتبة Cuckoo، التي تولّد فئات spy عبر SourceKit.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.