كيفية اختبار إشارات Django مثل الموالية

جهاز استقبال الإشارات أنا سعيد لأنني لست مضطرًا للاختبار

للحصول على تجربة قراءة أفضل ، راجع هذه المقالة على موقع الويب الخاص بي.

إشارات Django مفيدة للغاية لوحدات الفصل. إنها تتيح لتطبيق Django ذي المستوى المنخفض إرسال الأحداث لتطبيقات أخرى للتعامل معها دون إنشاء تبعية مباشرة.

من السهل إعداد الإشارات ، ولكن من الصعب اختبارها. لذا ، في هذه المقالة ، سوف أطلعك على تنفيذ مدير السياق لاختبار إشارات Django ، خطوة بخطوة.

حالة الاستخدام

دعنا نقول أن لديك وحدة دفع مع وظيفة رسوم. (أكتب الكثير عن المدفوعات ، لذلك أعرف حالة الاستخدام هذه جيدًا.) بمجرد فرض رسوم ، فأنت تريد زيادة إجمالي الرسوم.

كيف سيبدو ذلك باستخدام الإشارات؟

أولاً ، حدد الإشارة:

# الإشارات
من django.dispatch استيراد الإشارة
charge_completed = إشارة (provide_args = ['total'])

ثم أرسل الإشارة عند اكتمال الشحن بنجاح:

# الدفع
من. الإشارات استيراد charge_completed
classmethod
عملية def_charge (cls ، المجموع):
    # عملية المسؤول ...
    إذا النجاح:
        charge_completed.send_robust (
            مرسل = CLS،
            مجموع = المجموع،
        )

يمكن أن يستخدم تطبيق مختلف ، مثل تطبيق الملخص ، معالجًا يزيد عداد الرسوم الإجمالي:

# الملخص
من django.dispatch استقبال الاستيراد
من. الإشارات استيراد charge_completed
receiver (charge_completed)
def increment_total_charges (المرسل ، الإجمالي ، ** kwargs):
    total_charges + = الإجمالي

لا يلزم أن تعرف وحدة الدفع وحدة الملخص أو أي وحدة أخرى تتعامل مع الرسوم المكتملة. يمكنك إضافة العديد من أجهزة الاستقبال دون تعديل وحدة الدفع.

على سبيل المثال ، فيما يلي مرشحين جيدين لأجهزة الاستقبال:

  • تحديث حالة المعاملة.
  • إرسال إشعار بالبريد الإلكتروني للمستخدم.
  • تحديث آخر تاريخ استخدام لبطاقة الائتمان.

إشارات الاختبار

الآن بعد أن غطيت الأساسيات ، فلنكتب اختبارًا للمعالجة. تريد التأكد من إرسال الإشارة باستخدام الوسيطات الصحيحة عند اكتمال الشحن بنجاح.

أفضل طريقة لاختبار ما إذا تم إرسال إشارة هي الاتصال به:

# test.py
من django.test استيراد TestCase
من. دفع رسوم الاستيراد
من. الإشارات استيراد charge_completed
فئة TestCharge (TestCase):
    def test_should_send_signal_when_charge_succeeds (self):
        self.signal_was_called = خطأ
        self.total = لا شيء
        معالج def (المرسل ، الإجمالي ، ** kwargs):
            self.signal_was_called = صحيح
            self.total = الإجمالي
        charge_completed.connect (معالج)
        تهمة (100)
        self.assertTrue (self.signal_was_called)
        self.assertEqual (self.total، 100)
        charge_completed.disconnect (معالج)

نقوم بإنشاء معالج ، والاتصال بالإشارة ، وتنفيذ الوظيفة والتحقق من الحجج.

نستخدم الذات داخل المعالج لإنشاء إغلاق. إذا لم نستخدم الذات ، فستقوم وظيفة المعالج بتحديث المتغيرات في نطاقها المحلي ولن نتمكن من الوصول إليها. سنعود إلى هذا لاحقًا.

لنقم بإضافة اختبار للتأكد من عدم استدعاء الإشارة في حالة فشل الشحن:

def test_should_not_send_signal_when_charge_failed (self):
    self.signal_was_called = خطأ
    معالج def (المرسل ، الإجمالي ، ** kwargs):
        self.signal_was_called = صحيح
    charge_completed.connect (معالج)
    تهمة (-1)
    self.assertFalse (self.signal_was_called)
    charge_completed.disconnect (معالج)

هذا يعمل ولكنه كثير من الصلابة! يجب أن تكون هناك طريقة أفضل.

أدخل مدير السياق

لنفصل ما فعلناه حتى الآن:

  1. توصيل إشارة إلى بعض المعالج.
  2. قم بتشغيل رمز الاختبار وحفظ الوسائط التي تم تمريرها إلى المعالج.
  3. افصل المعالج عن الإشارة.

هذا النمط يبدو مألوفا ...

دعونا نلقي نظرة على ما يفعله مدير السياق المفتوح (ملف):

  1. فتح ملف.
  2. معالجة الملف.
  3. أغلق الملف.

ومدير سياق معاملة قاعدة البيانات:

  1. فتح الصفقة.
  2. تنفيذ بعض العمليات.
  3. إغلاق المعاملة (الالتزام / الاستعادة).

يبدو أن مدير السياق يمكنه العمل من أجل الإشارات أيضًا.

قبل أن تبدأ ، فكر كيف تريد استخدام مدير السياق لاختبار الإشارات:

مع CatchSignal (charge_completed) كـ sign_args
    تهمة (100)
self.assertEqual (إشارة_الأرقام. المجموع ، 100)

جميل ، دعنا نجربها:

فئة CatchSignal:
    def __init __ (self ، إشارة):
        الإشارة الذاتية
        self.signal_kwargs = {}
        def handler (مرسل ، ** kwargs):
            self.signal_kwrags.update (kwargs)
        مصراع ذاتي = معالج
    def __enter __ (self):
        self.signal.connect (self.handler)
        إرجاع self.signal_kwrags
    def __exit __ (self، exc_type، exc_value، tb):
        self.signal.disconnect (self.handler)

ما لدينا هنا:

  • لقد قمت بتهيئة السياق بالإشارة التي تريد "التقاطها".
  • ينشئ السياق وظيفة معالج لحفظ الوسائط التي ترسلها الإشارة.
  • يمكنك إنشاء إغلاق عن طريق تحديث كائن موجود (إشارة_كلمات) على الذات.
  • قمت بتوصيل المعالج للإشارة.
  • تتم بعض العمليات (عن طريق الاختبار) بين __enter__ و __exit__.
  • يمكنك قطع المعالج عن الإشارة.

دعنا نستخدم مدير السياق لاختبار وظيفة الشحن:

def test_should_send_signal_when_charge_succeeds (self):
    مع CatchSignal (charge_completed) كـ sign_args:
        تهمة (100)
    self.assertEqual (إشارة_الأرقام ['الإجمالي'] ، 100)

هذا أفضل ، لكن كيف سيبدو الاختبار السلبي؟

def test_should_not_send_signal_when_charge_failed (self):
    مع CatchSignal (إشارة) كإشارة_الأرقام:
        تهمة (100)
    self.assertEqual (signal_args، {})

ياك ، هذا سيء.

دعونا نلقي نظرة أخرى على المعالج:

  • نريد التأكد من استدعاء وظيفة المعالج.
  • نحن نريد اختبار args المرسلة إلى وظيفة المعالج.

انتظر ... أنا أعرف بالفعل هذه الوظيفة!

أدخل وهمية

دعنا نستبدل معالجنا بـ Mock:

من unittest استيراد وهمية
فئة CatchSignal:
    def __init __ (self ، إشارة):
        الإشارة الذاتية
        التعامل مع المصير
    def __enter __ (self):
        self.signal.connect (self.handler)
        العودة self.handler
    def __exit __ (self، exc_type، exc_value، tb):
        self.signal.disconnect (self.handler)

والاختبارات:

def test_should_send_signal_when_charge_succeeds (self):
    مع CatchSignal (charge_completed) كمعالج:
        تهمة (100)
    handler.assert_called_once_with (
        مجموعه = 100،
        مرسل = mock.ANY،
        إشارة = charge_completed،
    )
def test_should_not_send_signal_when_charge_failed (self):
    مع CatchSignal (charge_completed) كمعالج:
        تهمة (-1)
    handler.assert_not_called ()

أفضل بكثير!

لقد استخدمت محاكاة وهمية لما يجب استخدامه بالضبط ، ولا داعي للقلق بشأن النطاق والإغلاق.

الآن وقد أصبح لديك هذا العمل ، هل يمكنك أن تجعله أفضل؟

أدخل السياق

لدى Python وحدة مساعدة للتعامل مع مديري السياق تسمى contextlib.

دعنا نعيد كتابة سياقنا باستخدام contextlib:

من unittest استيراد وهمية
من سياق السياق استيراد السياق
contextmanager
def catch_signal (إشارة):
    "" "امسك إشارة django وأعد المكالمة الساخرة." ""
    معالج = mock.Mock ()
    signal.connect (معالج)
    العائد معالج
    signal.disconnect (معالج)

يعجبني هذا النهج لأنه أسهل في اتباعه:

  • يوضح العائد مكان تنفيذ رمز الاختبار.
  • لا حاجة إلى حفظ الكائنات بنفسها لأن رمز الإعداد (الدخول والخروج) في نفس النطاق.

وهذا كل شيء - 4 أسطر من التعليمات البرمجية لحكمهم جميعًا! الربح!