المحاكاة والبدائل: Mocks و Stubs
كيف نختبر دالةً تعتمد على الشبكة أو الوقت دون أن نلمسهما فعلًا؟ ⏱ نحو 45 دقيقة.
- تفرّق بين
StubوMockوFake. - تستبدل الاعتمادات الخارجية ببدائل.
- تستخدم
unittest.mockلمراقبة الاستدعاءات. - تحاكي الحدود لا المنطق.
١كيف نختبر ما يعتمد على الخارج؟
دالةٌ تستدعي بوابة دفعٍ عبر الإنترنت، أو تقرأ الوقت الحاليّ، أو ترسل بريدًا. كيف نختبرها؟ لا نريد دفعًا حقيقيًّا في كل اختبار، ولا اختبارًا يفشل لأن الشبكة بطيئة. الحلّ: نستبدل الاعتماد الخارجيّ ببديلٍ نتحكّم فيه.
| البديل | ماذا يفعل | مثال |
|---|---|---|
| Stub | يعيد قيمةً ثابتة مُجهَّزة | بوابة دفعٍ تعيد «نجح» دائمًا |
| Mock | بديلٌ نراقب كيف اُستُدعِي | نتأكّد أن «أرسل البريد» نودي مرّة |
| Fake | نسخةٌ مبسّطة عاملة | قاعدة بيانات في الذاكرة |
الفائدة: اختبارٌ سريع وحتميّ ومعزول عن العالم الخارجيّ. نختبر منطقنا لا خدمة الدفع، ونتحكّم في كل سيناريو (نجاح، فشل، خطأ شبكة).
٢Stub: استبدل بقيمةٍ ثابتة
دالةٌ تطبّق خصم كبار العملاء بناءً على رصيدٍ من خدمةٍ خارجية. نُمرّر الخدمة معاملًا فنستبدلها في الاختبار (حقن الاعتماد):
def final_price(price, loyalty_service): points = loyalty_service() # اعتماد خارجيّ return price * 0.9 if points >= 100 else price # في الإنتاج: loyalty_service يستعلم من قاعدة بيانات.# في الاختبار: stub يعيد قيمةً ثابتة — سريع وحتميّ.def stub_vip(): return 150 # عميل مميّزdef stub_new(): return 10 # عميل جديد print("مميّز:", final_price(200, stub_vip)) # خصم: 180print("جديد:", final_price(200, stub_new)) # بلا خصم: 200
الـ stub عزل الاختبار عن قاعدة البيانات تمامًا. نتحكّم في «النقاط» فنختبر فرعي الخصم بسهولةٍ وسرعة.
٣Mock: راقب كيف اُستُدعِي
أحيانًا المهمّ ليس القيمة المُعادة بل أن شيئًا حدث: هل أُرسِل بريد التأكيد؟ Mock يسجّل الاستدعاءات لنتحقّق منها. بايثون يقدّم unittest.mock:
from unittest.mock import Mock def checkout(cart_total, send_email): if cart_total > 0: send_email("تم الطلب") return "مكتمل" # mock بديلٌ نراقبهnotifier = Mock()result = checkout(300, notifier) # نتحقّق من السلوك لا القيمةprint("النتيجة:", result)print("أُرسِل البريد؟", notifier.called)print("مرّات الإرسال:", notifier.call_count)notifier.assert_called_once_with("تم الطلب") # يفشل إن لم يُستدعَ هكذاprint("✓ البريد أُرسِل مرّةً بالرسالة الصحيحة")
توقّع وتحقّق
البديل الذي يعيد قيمةً ثابتة:
البديل الذي نراقب كيف اُستُدعِي:
نحاكي الاعتمادات:
فائدة المحاكاة:
الخلاصة
- لاختبار ما يعتمد على الخارج نستبدله ببديلٍ نتحكّم فيه.
Stubيعيد قيمةً ثابتة، وMockيراقب كيف اُستُدعِي، وFakeنسخةٌ مبسّطة عاملة.unittest.mockيسجّل الاستدعاءات(called, call_count, assert_called...).- حاكِ الاعتمادات الخارجية فقط؛ محاكاة كل شيءٍ تفرّغ الاختبار من معناه.
تحدٍّ برمجي: راقب الاستدعاء
متقدمالدالة process تستدعي logger حين يكون المبلغ سالبًا. استخدم Mock وتحقّق أنه اُستُدعِي مرّةً لمبلغٍ سالب، واطبع عدد مرّات الاستدعاء.
- مرّر
Mock()كـlogger، ونفّذprocess(-5, logger). - اطبع
logger.call_count.
ابدأ من هذا الكود، واضغط ▶ شغّل هنا لتكتب حلّك وتجرّبه مباشرة:
from unittest.mock import Mockdef process(amount, logger): if amount < 0: logger("مبلغ سالب") return abs(amount)# استخدم Mock ونفّذ process(-5, ...)، ثم اطبع عدد مرّات الاستدعاء
💡 تلميح 1
logger = Mock() ثم process(-5, logger).💡 تلميح 2
print(logger.call_count).حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
from unittest.mock import Mockdef process(amount, logger): if amount < 0: logger("مبلغ سالب") return abs(amount)logger = Mock()process(-5, logger)print(logger.call_count)