يصبح اختبار مواقع الويب أصعب يدويًا عند نموها، إذ يصبح هناك مزيدٌ من الأمور لاختبارها، وتصبح التفاعلات بين المكونات أكثر تعقيدًا، إذ يمكن أن يؤثر تغيير بسيط في منطقةٍ ما على مناطق أخرى، لذلك ستكون هناك حاجة إلى مزيد من التغييرات لضمان استمرار عمل كل شيء بنجاح وعدم ظهور أخطاء عند إجراء مزيدٍ من التغييرات. تتمثل إحدى طرق التخفيف من هذه المشاكل في كتابة اختبارات آلية، والتي يمكن تشغيلها بسهولة وموثوقية في كل مرة تجري فيها تغييرًا. يوضح هذا المقال كيفية أتمتة اختبار الوحدة Unit Testing لموقعك باستخدام إطار اختبار جانغو Django.
- المتطلبات الأساسية: اطّلع على جميع المقالات السابقة من هذه السلسلة، بما في ذلك المقال الخاص بالعمل مع الاستمارات.
- الهدف: فهم كيفية كتابة اختبارات الوحدة لمواقع الويب المستندة إلى جانغو.
يحتوي موقع المكتبة المحلية Local Library حاليًا على صفحات لعرض قوائم بجميع الكتب والمؤلفين، والعروض التفصيلية لعناصر Book
و Author
، وصفحة لتجديد عناصر BookInstance
، وصفحات لإنشاء عناصر Author
وتحديثها وحذفها، إضافةً إلى سجلات Book
أيضًا إذا أكملت قسم التحدي في المقال السابق.
يمكن أن يستغرق التنقل يدويًا إلى كل صفحة والتحقق السطحي من أن كل شيء يعمل كما هو متوقع عدة دقائق حتى مع هذا الموقع الصغير نسبيًا، وسيزداد الوقت المطلوب للتحقق يدويًا من أن كل شيء يعمل بصورة صحيحة مع إجراء التغييرات ونمو الموقع. إذا استمرينا على هذا النحو، فسنقضي في النهاية معظم وقتنا في الاختبار ووقتًا قصيرًا جدًا في تحسين شيفرتنا البرمجية.
يمكن أن تساعد الاختبارات الآلية في حل هذه المشكلة، إذ يمكن تشغيلها بصورة أسرع بكثير من الاختبارات اليدوية، ويمكن اختبارها بمستوًى أقل من التفاصيل، واختبار الوظيفة نفسها تمامًا في كل مرة (لا يُعَد المختبرون البشر قريبين من هذه الموثوقية). يمكن تنفيذ هذه الاختبارات اليدوية بصورة أكثر انتظامًا لأنها سريعة، وإذا فشل الاختبار، فسيؤشّر بدقة إلى المكان الذي لا تعمل فيه الشيفرة البرمجية كما هو متوقع.
يمكن أن تعمل الاختبارات الآلية بوصفها أول مستخدم من العالم الحقيقي لشيفرتك البرمجية، مما يجبرك على أن تكون صارمًا في تحديد وتوثيق كيف ينبغي أن يتصرف موقعك، وتكون في أغلب الأحيان أساسًا لأمثلة شيفرتك البرمجية وتوثيقها. لذا تبدأ بعض عمليات تطوير البرمجيات بتعريف الاختبار وتقديمه، ثم تُكتَب الشيفرة البرمجية لمطابقة السلوك المطلوب، مثل التطوير المُقاد بالاختبار Test-Driven Development والتطوير المُقاد بالسلوك Behavior-Driven Development.
يوضح هذا المقال كيفية كتابة الاختبارات الآلية لإطار عمل جانغو من خلال إضافة عدد من الاختبارات إلى موقع المكتبة المحلية LocalLibrary.
تتألف هذه السلسلة الفرعية من السلسلة الأشمل تعلم تطوير الويب من المقالات التالية:
- مدخل إلى إطار عمل الويب جانغو Django
- إعداد بيئة تطوير تطبيقات جانغو
- تطبيق عملي لتعلم جانغو - الجزء الأول: إنشاء موقع ويب هيكلي لمكتبة محلية
- تطبيق عملي لتعلم جانغو - الجزء الثاني: استخدام النماذج Models
- تطبيق عملي لتعلم جانغو - الجزء الثالث: موقع مدير جانغو Django Admin
- تطبيق عملي لتعلم جانغو - الجزء الرابع: إنشاء صفحة المكتبة الرئيسية
- تطبيق عملي لتعلم جانغو - الجزء الخامس: العروض Views العامة والتفصيلية
- تطبيق عملي لتعلم جانغو - الجزء السادس: إدارة الجلسات Sessions
- تطبيق عملي لتعلم جانغو - الجزء السابع: استيثاق المستخدمين وأذوناتهم
- تطبيق عملي لتعلم جانغو - الجزء الثامن: العمل مع الاستمارات Forms
- تطبيق عملي لتعلم جانغو - الجزء التاسع: اختبار تطبيق جانغو
- تطبيق عملي لتعلم جانغو - الجزء 10: نشر تطبيق جانغو في بيئة الإنتاج
- تعرف على أمان تطبيقات جانغو
أنواع الاختبارات
هناك العديد من الأنواع والمستويات والتصنيفات للاختبارات وأساليبها، وأهم الاختبارات الآلية هي:
- اختبارات الوحدة Unit Tests: التي تتحقق من السلوك الوظيفي للمكونات الفردية على مستوى الأصناف والدوال غالبًا.
- الاختبارات التراجعية Regression Tests: هي الاختبارات التي تعيد إنتاج أخطاء قديمة، إذ يُشغَّل كل اختبار للتحقق من إصلاح الخطأ في البداية، ثم يُعَاد تشغيله للتأكد من عدم ظهوره مرةً أخرى بعد إجراء تغييرات لاحقة على الشيفرة البرمجية.
- اختبارات التكامل Integration Tests: تتحقق من كيفية عمل مجموعات المكونات عند استخدامها مع بعضها بعضًا، إذ تكون اختبارات التكامل على دراية بالتفاعلات المطلوبة بين المكونات، ولكنها ليست بالضرورة على دراية بالعمليات الداخلية لكل مكون. يمكن أن تغطي اختبارات التكامل مجموعات بسيطة من المكونات في موقع الويب الكامل.
ملاحظة: تشمل أنواع الاختبارات الشائعة الأخرى اختبارات الصندوق الأسود والصندوق الأبيض والاختبارات اليدوية والآلية واختبارات الكناري Canary والدخان Smoke والمطابقة والقبول والوظيفية والنظام والأداء والحِمل واختبارات الإجهاد.
ماذا يقدم جانغو لعملية الاختبار؟
يُعَد اختبار موقع الويب مهمةً معقدة، لأنه يتكون من عدة طبقات من الشيفرة البرمجية، بدءًا من معالجة الطلبات على مستوى طلبات HTTP، إلى استعلامات النموذج، إلى التحقق من صحة الاستمارة ومعالجتها، وإظهار القالب.
يوفّر جانغو إطار عمل للاختبار مع تسلسل هرمي صغير من الأصناف التي تعتمد على مكتبة بايثون المعيارية unittest
، إذ يُعَد إطار عمل الاختبار هذا مناسبًا لكل من اختبارات الوحدة والتكامل، فهو يضيف توابع وأدوات واجهة برمجة التطبيقات API للمساعدة في اختبار الويب وسلوك جانغو المُحدَّد، مما يسمح لك بمحاكاة الطلبات وإدخال بيانات الاختبار وفحص خرج تطبيقك. يوفر جانغو أيضًا واجهة برمجة تطبيقات (LiveServerTestCase) وأدوات لاستخدام أطر عمل اختبار مختلفة، فمثلًا يمكنك التكامل مع إطار عمل سيلينيوم Selenium الشهير لمحاكاة مستخدم يتفاعل مع متصفح مباشرةً.
يمكنك كتابة اختبار من خلال اشتقاق أيٍّ من أصناف اختبار جانغو الأساسية unittest، مثل SimpleTestCase
و TransactionTestCase
و TestCase
و LiveServerTestCase
، ثم كتابة توابع منفصلة للتحقق من أن وظيفة معينة تعمل كما هو متوقع، إذ تستخدم الاختبارات توابع "assert" لاختبار أن ناتج التعابير هو القيمة True
أو القيمة False
أو تساوي قيمتين أو غير ذلك.
ينفّذ إطار العمل توابع الاختبار المختارة في الأصناف المشتقة عند بدء تشغيل الاختبار، إذ تُشغَّل توابع الاختبار بطريقة مستقلة مع سلوك الإعداد و/أو الهدم tear-down الشائع المُعرَّف في الصنف كما هو موضح فيما يلي:
class YourTestClass(TestCase): def setUp(self): # تشغيل الإعداد قبل كل تابع اختبار pass def tearDown(self): # تنظيف التشغيل بعد كل تابع اختبار pass def test_something_that_will_pass(self): self.assertFalse(False) def test_something_that_will_fail(self): self.assertTrue(False)
أفضل صنف أساسي لمعظم الاختبارات هو الصنف django.test.TestCase، الذي ينشئ قاعدة بيانات نظيفة قبل تشغيل اختباراته، ويشغِّل كل دالة اختبار في تعاملاته الخاصة. يمتلك هذا الصنف أيضًا صنف الاختبار Client الذي يمكنك استخدامه لمحاكاة مستخدم يتفاعل مع الشيفرة البرمجية على مستوى العرض View. سنركز في الأقسام التالية على اختبارات الوحدة المُنشَأة باستخدام صنف TestCase الأساسي.
ملاحظة: يُعَد الصنف django.test.TestCase سهل الاستخدام، ولكنه يمكن أن يؤدي إلى أن تكون بعض الاختبارات أبطأ مما يجب، إذ لن يحتاج كل اختبار إلى إعداد قاعدة بياناته أو محاكاة تفاعل العرض. قد ترغب في استبدال بعض اختباراتك بأصناف الاختبار الأبسط المتاحة بعد أن تتعرف على ما يمكنك تطبيقه باستخدام هذا الصنف.
ماذا يجب أن تختبر؟
يجب أن تختبر جميع جوانب شيفرتك البرمجية، ولكن لا حاجة إلى اختبار أيّ مكتبات أو وظائف متوفرة مثل جزء من بايثون أو جانغو.
ليكن لدينا مثلًا نموذج المؤلف Author
الآتي، فلا حاجة لاختبار التخزين الصحيح للحقلين first_name
و last_name
بوصفهما حقلين من النوع CharField
في قاعدة البيانات صراحةً لأن ذلك شيء حدّده جانغو، بالرغم من أنك حتمًا ستختبر هذه الوظيفة أثناء التطوير، ولا حاجة أيضًا لاختبار التحقق من أن الحقل date_of_birth
حقل تاريخ، لأنه شيء طبّقه جانغو، لكن ينبغي التحقق من النص المستخدم مع التسميات "First name" و "Last name" و "Date of birth" و "Died" وحجم الحقل المخصص للنص (100 محرف)، لأنها جزء من تصميمك وشيء يمكن أن يُكسَر أو يتغيّر في المستقبل.
class Author(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) date_of_birth = models.DateField(null=True, blank=True) date_of_death = models.DateField('Died', null=True, blank=True) def get_absolute_url(self): return reverse('author-detail', args=[str(self.id)]) def __str__(self): return '%s, %s' % (self.last_name, self.first_name)
ينبغي أيضًا التحقق من أن التابعين المُخصَّصين get_absolute_url()
و __str__()
يتصرفان بالطريقة المطلوبة لأنهما يمثلان شيفرتك أو منطق عملك. يمكنك في حالة التابع get_absolute_url()
الوثوق بتقديم تابع جانغو reverse()
بصورة صحيحة، لذا فإنّ ما تختبره هو تعريف العرض المرتبط به.
ملاحظة: يمكن أن يلاحظ القراء المتمرسون أننا نرغب في تقييد تاريخ الميلاد والوفاة بقيم مناسبة، والتحقق من أن الوفاة تأتي بعد الميلاد، ويمكن تحقيق هذا التقييد في جانغو من خلال إضافته إلى أصناف استمارتك. يمكنك تعريف أدوات تحقق لحقول النموذج وأدوات تحقق من النماذج نفسها، ولكنها تُستخدَم فقط على مستوى الاستمارة إذا استدعاها التابع clean()
الخاص بالنموذج، وهذا يتطلب الصنف ModelForm
، أو استدعاء التابع clean()
الخاص بالنموذج على وجه التحديد.
لنبدأ الآن في التعرّف على كيفية تعريف الاختبارات وتشغيلها.
نظرة عامة على بنية الاختبار
لنلقِ أولًا نظرة سريعة على مكان وكيفية تعريف الاختبارات قبل أن ندخل في تفاصيل ما الذي يجب اختباره.
يستخدم جانغو اكتشاف الاختبار المبني مسبقًا في الوحدة unittest، والذي سيكتشف الاختبارات ضمن مجلد العمل الحالي في أي ملف يسمى بالنمط test*.py، إذ يمكنك استخدام أي بنية تريدها شريطة أن تسمي الملفات بصورة مناسبة. نوصي بإنشاء وحدة لشيفرة اختبارك، وأن يكون لديك ملفات منفصلة للنماذج Modelsوالعروض Views والاستمارات Forms وأيّ أنواع أخرى من الشيفرة البرمجية التي يجب اختبارها كما يلي:
catalog/ /tests/ __init__.py test_models.py test_forms.py test_views.py
أنشئ بنية الملفات السابقة في مشروع المكتبة المحلية LocalLibrary، وينبغي أن يكون الملف "init.py" فارغًا، مما يخبر بايثون أن المجلد هو حزمة package. يمكنك إنشاء ملفات الاختبار الثلاثة من خلال نسخ وإعادة تسمية ملف الاختبار الهيكلي "/catalog/tests.py".
ملاحظة: أُنشِئ ملف اختبار الهيكلي /catalog/tests.py تلقائيًا عند بناء موقع ويب جانغو الهيكلي، إذ يُعَد وضع جميع اختباراتك ضمنه أمرًا "مسموحًا به"، ولكن إذا أجريت الاختبار بصورة صحيحة، فسينتهي بك الأمر سريعًا مع ملف اختبار كبير جدًا ولا يمكن إدارته، لذا احذف الملف الهيكلي لأننا لن نحتاج إليه.
افتح الملف "/catalog/tests/test_models.py"، الذي ينبغي أن يستورد الصنف django.test.TestCase
كما يلي:
from django.test import TestCase # أنشئ اختباراتك هنا
ستضيف غالبًا صنفَ اختبار لكل نموذج/عرض/استمارة تريد اختبارها باستخدام توابع فردية لاختبار وظائف معينة، ويمكن أن ترغب في حالات أخرى في الحصول على صنف منفصل لاختبار حالة استخدام محددة باستخدام دوال اختبار فردية تختبر جوانب حالة الاستخدام، مثل صنف اختبار صحة حقل النموذج بصورة صحيحة باستخدام دوال لاختبار كلٍّ من حالات الفشل المحتملة. تُعَد البنية أمرًا متروكًا لك، ولكن يُفضَّل أن تكون متناسقة.
أضِف صنف الاختبار التالي إلى نهاية الملف، إذ يوضح هذا الصنف كيفية بناء صنف حالة اختبار من خلال اشتقاق الصنف TestCase
:
class YourTestClass(TestCase): @classmethod def setUpTestData(cls): print("setUpTestData: Run once to set up non-modified data for all class methods.") pass def setUp(self): print("setUp: Run once for every test method to setup clean data.") pass def test_false_is_false(self): print("Method: test_false_is_false.") self.assertFalse(False) def test_false_is_true(self): print("Method: test_false_is_true.") self.assertTrue(False) def test_one_plus_one_equals_two(self): print("Method: test_one_plus_one_equals_two.") self.assertEqual(1 + 1, 2)
يعرّف الصنف الجديد تابعين يمكنك استخدامهما لضبط الاختبار المسبق لإنشاء أيّ نماذج أو كائنات أخرى ستحتاجها للاختبار مثلًا، وهما:
-
setUpTestData()
: يُستدعَى مرةً واحدة في بداية تشغيل الاختبار للإعداد على مستوى الصنف، ويمكنك استخدامه لإنشاء كائنات لن تُعدَّل أو تُغيَّر في أيٍّ من توابع الاختبار. -
setUp()
: يُستدعَى قبل دوال الاختبار لإعداد الكائنات التي يمكن أن يعدّلها الاختبار، وستحصل كل دالة اختبار على نسخة "جديدة" من هذه الكائنات.
ملاحظة: تحتوي أصناف الاختبار على تابع tearDown()
التي لم نستخدمه، ولا يُعَد هذا التابع مفيدًا بخاصة لاختبارات قاعدة البيانات، لأن الصنف الأساسي TestCase
يهتم بهدم قاعدة البيانات نيابةً عنك.
لدينا فيما يلي عددٌ من توابع الاختبار التي تستخدم دوال Assert
لاختبار ما إذا كانت الشروط صحيحة أو خاطئة أو متساوية، وهي: AssertTrue
و AssertFalse
و AssertEqual
. إذا لم يُقيَّم الشرط كما هو متوقع، فسيفشل الاختبار وسيرسَل الخطأ إلى طرفيتك.
تُعَد AssertTrue
و AssertFalse
و AssertEqual
تأكيدات معيارية توفّرها المكتبة unittest، وهناك تأكيدات معيارية أخرى في إطار العمل، وتأكيدات خاصة بجانغو لاختبار ما إذا كان العرض يعيد التوجيه assertRedirects
، أو يستخدم قالب معين assertTemplateUsed
وغير ذلك.
ملاحظة: لا ينبغي عادةً تضمين دوال print()
في اختباراتك كما هو موضح سابقًا، لكننا نطبّق ذلك في القسم التالي فقط لتتمكّن من رؤية ترتيب استدعاء دوال الإعداد في الطرفية.
كيفية تشغيل الاختبارات
أسهل طريقة لتشغيل جميع الاختبارات هي استخدام الأمر التالي:
python3 manage.py test
سيؤدي هذا الأمر إلى اكتشاف جميع الملفات المُسمَّاة باستخدام النمط test*.py في المجلد الحالي وتشغيل جميع الاختبارات التي تعرّفها أصناف أساسية مناسبة، فلدينا هنا عدد من ملفات الاختبار، ولكن يحتوي الملف /catalog/tests/test_models.py فقط على الاختبارات حاليًا. ستقدم الاختبارات افتراضيًا تقريرًا فرديًا فقط عن حالات فشل الاختبار ويتبعه ملخص الاختبار.
ملاحظة: إذا حصلتَ على أخطاء مشابهة للخطأ التالي:
ValueError: Missing staticfiles manifest entry...
يمكن أن يكون هذا الخطأ بسبب عدم تشغيل الاختبار للأمر collectstatic
افتراضيًا، وبسبب أن تطبيقك يستخدم صنف تخزين يتطلب ذلك. اطلع على manifest_strict لمزيد من المعلومات. هناك عدد من الطرق التي يمكنك من خلالها التغلب على هذه المشكلة، وأسهلها هو تشغيل الأمر collectstatic
قبل تشغيل الاختبارات كما يلي:
python3 manage.py collectstatic
شغّل الاختبارات في المجلد الجذر لمشروع المكتبة المحلية LocalLibrary، ويجب أن ترى خرجًا مثل الخرج التالي:
> python3 manage.py test Creating test database for alias 'default'... setUpTestData: Run once to set up non-modified data for all class methods. setUp: Run once for every test method to setup clean data. Method: test_false_is_false. setUp: Run once for every test method to setup clean data. Method: test_false_is_true. setUp: Run once for every test method to setup clean data. Method: test_one_plus_one_equals_two. . ====================================================================== FAIL: test_false_is_true (catalog.tests.tests_models.YourTestClass) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\GitHub\django_tmp\library_w_t_2\locallibrary\catalog\tests\tests_models.py", line 22, in test_false_is_true self.assertTrue(False) AssertionError: False is not true ---------------------------------------------------------------------- Ran 3 tests in 0.075s FAILED (failures=1) Destroying test database for alias 'default'...
نرى في الخرج السابق أن لدينا فشل اختبار واحد، ويمكننا أن نرى فعلًا الدالة التي فشلت وسبب فشلها، إذ يُعَد هذا الفشل متوقعًا، لأن القيمة False
ليست القيمة True
.
ملاحظة: أهم شيء يجب تعلّمه من خرج الاختبار السابق هو أنه يُفضَّل استخدام أسماء وصفية أو ذات معنًى لكائناتك وتوابعك.
يُظهِر خرج دوال print()
كيفية استدعاء تابع setUpTestData()
مرةً واحدةً للصنف واستدعاء التابع setUp()
قبل كل تابع. تذكّر أنك لن تضيف عادةً هذا النوع من دوال print()
إلى اختباراتك.
توضّح الأقسام التالية كيفية تشغيل اختبارات محددة، وكيفية التحكم في مقدار المعلومات التي تعرضها الاختبارات.
عرض مزيد من معلومات الاختبار
إذا أردتَ الحصول على مزيد من المعلومات حول تشغيل الاختبار، فيمكنك تغيير قيمة verbosity
، فمثلًا يمكنك ضبطها على القيمة "2" لسرد حالات نجاح الاختبار إضافةً إلى حالات الفشل ومجموعة كاملة من المعلومات حول كيفية إعداد قاعدة بيانات الاختبار:
python3 manage.py test --verbosity 2
مستويات verbosity
المسموح بها هي: 0 و 1 و 2 و 3 والقيمة الافتراضية هي "1".
تسريع الاختبارات
إذا كانت اختباراتك مستقلة، فيمكنك تسريعها على جهاز متعدد المعالجات من خلال تشغيلها على التوازي، إذ يؤدي استخدام الأمر test --parallel auto
الآتي إلى تشغيل عملية اختبار واحدة لكل نواة متاحة، ويُعَد استخدام auto
اختياريًا، ويمكنك تحديد عدد معين من الأنوية لاستخدامها.
python3 manage.py test --parallel auto
اطّلع على DJANGO_TEST_PROCESSES لمزيدٍ من المعلومات، بما في ذلك ما يجب عليك فعله إن لم تكن اختباراتك مستقلة.
تشغيل اختبارات محددة
إذا أردتَ تشغيل مجموعة فرعية من اختباراتك، فيمكنك ذلك من خلال تحديد المسار النقطي الكامل للحزمة (أو لمجموعة الحزم) أو الوحدة أو الصنف TestCase
الفرعي أو التابع كما يلي:
# تشغيل الوحدة المُحدَّدة python3 manage.py test catalog.tests # تشغيل الوحدة المُحدَّدة python3 manage.py test catalog.tests.test_models # تشغيل الصنف المُحدَّد python3 manage.py test catalog.tests.test_models.YourTestClass # تشغيل التابع المُحدَّد python3 manage.py test catalog.tests.test_models.YourTestClass.test_one_plus_one_equals_two
خيارات مشغل الاختبارات الأخرى
يوفر مشغِّل الاختبارات العديد من الخيارات الأخرى، بما في ذلك القدرة على خلط الاختبارات --shuffle
، وتشغيلها في وضع تنقيح الأخطاء --debug-mode
، واستخدام مسجِّل بايثون Python logger لالتقاط النتائج. اطلع على توثيق مشغّل اختبار جانغو لمزيد من المعلومات، كما يمكنك الاطلاع على المقال التالي كيف تستخدم التسجيل Logging في بايثون 3 على أكاديمية حسوب لمزيدٍ من المعلومات حوال التسجيل.
اختبارات موقع المكتبة المحلية LocalLibrary
نعرف الآن كيفية تشغيل اختباراتنا ونوع الأمور التي يجب اختبارها، ولنلقِ الآن نظرةً على بعض الأمثلة العملية.
ملاحظة: لن نكتب جميع الاختبارات الممكنة، ولكن سيعطيك هذا القسم فكرةً عن كيفية عمل الاختبارات، وما الأمور الأخرى التي يمكنك تطبيقها.
النماذج Models
يجب أن نختبر أيّ شيء يمثل جزءًا من تصميمنا أو أي شيء تعرّفه الشيفرة البرمجية التي كتبناها بدون المكتبات أو الشيفرة البرمجية التي اختبرها جانغو أو فريق تطوير بايثون مسبقًا.
ليكن لدينا نموذج المؤلف Author
الآتي، إذ يجب أن نختبر تسميات جميع الحقول، لأن لدينا تصميمًا يوضّح ما يجب أن تكون عليه هذه القيم بالرغم من أننا لم نحدّد معظمها صراحةً، فإن لم نختبر هذه القيم، فلن نعرف أن تسميات الحقول لها قيمها المقصودة الخاصة. وبالمثل، نعرف أن جانغو سيُنشِئ حقلًا بطول محدد، ولكن يُفضَّل تحديد اختبار لهذا الطول للتأكد من تطبيقه كما هو مُخطَّط له.
class Author(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) date_of_birth = models.DateField(null=True, blank=True) date_of_death = models.DateField('Died', null=True, blank=True) def get_absolute_url(self): return reverse('author-detail', args=[str(self.id)]) def __str__(self): return f'{self.last_name}, {self.first_name}'
افتح الملف "/catalog/tests/test_models.py"، وضع شيفرة الاختبار الآتية لنموذج المؤلف Author
مكان أي شيفرة برمجية موجودة مسبقًا، إذ سترى أننا نستورد الصنف TestCase
أولًا ونشتق منه صنف الاختبار AuthorModelTest
باستخدام اسم وصفي لنتمكّن بسهولة من تحديد أيّ اختبارات فاشلة في خرج الاختبار، ثم نستدعي التابع setUpTestData()
لإنشاء كائن المؤلف الذي سنستخدمه دون تعديل في أيٍّ من الاختبارات.
from django.test import TestCase from catalog.models import Author class AuthorModelTest(TestCase): @classmethod def setUpTestData(cls): # إعداد الكائنات غير المُعدَّلة التي تستخدمها جميع توابع الاختبار Author.objects.create(first_name='Big', last_name='Bob') def test_first_name_label(self): author = Author.objects.get(id=1) field_label = author._meta.get_field('first_name').verbose_name self.assertEqual(field_label, 'first name') def test_date_of_death_label(self): author = Author.objects.get(id=1) field_label = author._meta.get_field('date_of_death').verbose_name self.assertEqual(field_label, 'died') def test_first_name_max_length(self): author = Author.objects.get(id=1) max_length = author._meta.get_field('first_name').max_length self.assertEqual(max_length, 100) def test_object_name_is_last_name_comma_first_name(self): author = Author.objects.get(id=1) expected_object_name = f'{author.last_name}, {author.first_name}' self.assertEqual(str(author), expected_object_name) def test_get_absolute_url(self): author = Author.objects.get(id=1) # سيفشل هذا الاختبار أيضًا إن لم يكن urlconf مُعرَّفًا self.assertEqual(author.get_absolute_url(), '/catalog/author/1')
تتحقق اختبارات الحقول من أن قيم تسميات الحقول verbose_name
وأن حجم الحقول المحرفية كما هو متوقع. تمتلك جميع هذه التوابع أسماءً وصفية وتتبع النمط نفسه:
# الحصول على كائن مؤلف لاختباره author = Author.objects.get(id=1) # الحصول على البيانات الوصفية للحقل المطلوب واستخدامها للاستعلام عن بيانات الحقل المطلوبة field_label = author._meta.get_field('first_name').verbose_name # موازنة القيمة بالنتيجة المتوقعة self.assertEqual(field_label, 'first name')
الأمور المهمة التي يجب ملاحظتها هي:
-
لا يمكننا الحصول على
verbose_name
مباشرةً باستخدامauthor.first_name.verbose_name
، لأنauthor.first_name
هو سلسلة نصية أي ليس مؤشرًا للكائنfirst_name
الذي يمكننا استخدامه للوصول إلى خاصياته، لذا ينبغي بدلًا من ذلك استخدام السمة_meta
الخاصة بالمؤلف للحصول على نسخة من الحقل واستخدامها للاستعلام عن المعلومات الإضافية. -
اخترنا استخدام
assertEqual(field_label,'first name')
عوضًا عنassertTrue(field_label == 'first name')
، والسبب في ذلك هو أن خرجassertEqual
يخبرك عن التسمية الفعلية في حالة فشل الاختبار، مما يجعل تنقيح أخطاء المشكلة أسهل قليلًا.
ملاحظة: أُهمِلت اختبارات تسميات last_name
و date_of_birth
واختبار طول الحقل last_name
، لذا ضِف نسخك الخاصة الآن باتباع اصطلاحات التسمية والأساليب الموضَّحة سابقًا.
يجب أيضًا اختبار توابعنا الخاصة، فمثلًا تتحقق الاختبارات التالية من بناء اسم الكائن كما توقعنا باستخدام تنسيق "Last Name", "First Name"، وأن عنوان URL الذي نحصل عليه لعنصر المؤلف Author
كما هو متوقع:
def test_object_name_is_last_name_comma_first_name(self): author = Author.objects.get(id=1) expected_object_name = f'{author.last_name}, {author.first_name}' self.assertEqual(str(author), expected_object_name) def test_get_absolute_url(self): author = Author.objects.get(id=1) # سيفشل هذا الاختبار أيضًا إن لم يكن urlconf مُعرَّفًا self.assertEqual(author.get_absolute_url(), '/catalog/author/1')
شغّل هذه الاختبارات الآن. إذا أنشأت نموذج المؤلف كما وضّحنا سابقًا، فيُحتمَل أن تحصل على خطأ للتسمية date_of_death
كما هو موضح فيما يلي، إذ يفشل الاختبار لأنه مكتوب مع التوقع بأن يتبع تعريف التسمية اصطلاح جانغو بعدم كتابة الحرف الأول من التسمية بأحرف كبيرة، إذ يطبّق جانغو ذلك نيابةً عنك:
====================================================================== FAIL: test_date_of_death_label (catalog.tests.test_models.AuthorModelTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\...\locallibrary\catalog\tests\test_models.py", line 32, in test_date_of_death_label self.assertEqual(field_label,'died') AssertionError: 'Died' != 'died' - Died ? ^ + died ? ^
يُعَد ذلك خطأً بسيطًا جدًا، ولكنه يسلط الضوء على كيفية تحقّق كتابة الاختبارات من أيّ افتراضات قد وضعتها بدقة أكبر.
ملاحظة: عدّل تسمية الحقل date_of_death
في الملف "/catalog/models.py" إلى "died" وأعِد تشغيل الاختبارات.
تتشابه أنماط اختبار النماذج الأخرى، لذا لن نستكمل مناقشة هذه النماذج، ولكن لا تتردد في إنشاء اختباراتك الخاصة للنماذج الأخرى.
الاستمارات Forms
يُعَد نهج اختبار استماراتك نفس نهج لاختبار نماذجك، إذ ينبغي اختبار أيّ شيء كتبتَ شيفرته البرمجية أو أي شيء يحدده تصميمك، ولكن لا تحتاج إلى اختبار سلوك إطار العمل الأساسي والمكتبات الخارجية الأخرى، لذا يجب اختبار احتواء الاستمارة على الحقول التي تريدها، وأنها معروضةٌ مع التسميات ونص التعليمات المناسب. لا تحتاج إلى التحقق من أن جانغو يتحقق من صحة نوع الحقل بصورة صحيحة، إلّا إذا أنشأت حقلك المُخصَّص مع التحقق من صحته، فلست بحاجة مثلًا لاختبار أن حقل البريد الإلكتروني لا يقبل سوى البريد الإلكتروني، ولكن يمكن أن تحتاج إلى اختبار التحقق من صحة البيانات الإضافي الذي تتوقع إجراءه على الحقول ورسائل الأخطاء التي ستولّدها شيفرتك البرمجية.
لتكن لدينا استمارة تجديد الكتب التي تحتوي على حقل واحد فقط لتاريخ التجديد، والذي سيكون له تسمية ونص تعليمات يجب التحقق منه:
class RenewBookForm(forms.Form): """Form for a librarian to renew books.""" renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).") def clean_renewal_date(self): data = self.cleaned_data['renewal_date'] # تحقق من أن التاريخ ليس في الماضي if data < datetime.date.today(): raise ValidationError(_('Invalid date - renewal in past')) # تحقق من أن التاريخ موجود ضمن المجال المسموح به (+4 أسابيع من اليوم) if data > datetime.date.today() + datetime.timedelta(weeks=4): raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead')) # تذكّر دائمًا أن تعيد البيانات النظيفة return data
افتح الملف "/catalog/tests/test_forms.py" وضع الاختبار التالي لاستمارة RenewBookForm
مكان أيّ شيفرة برمجية موجودة مسبقًا. نبدأ باستيراد استمارتنا وبعض مكتبات بايثون وجانغو للمساعدة في اختبار الوظائف المتعلقة بالوقت، ثم نصرّح عن صنف اختبار الاستمارة بالطريقة نفسها التي طبّقناها على النماذج باستخدام اسم وصفي لصنف الاختبار المشتق من الصنف TestCase
.
import datetime from django.test import TestCase from django.utils import timezone from catalog.forms import RenewBookForm class RenewBookFormTest(TestCase): def test_renew_form_date_field_label(self): form = RenewBookForm() self.assertTrue(form.fields['renewal_date'].label is None or form.fields['renewal_date'].label == 'renewal date') def test_renew_form_date_field_help_text(self): form = RenewBookForm() self.assertEqual(form.fields['renewal_date'].help_text, 'Enter a date between now and 4 weeks (default 3).') def test_renew_form_date_in_past(self): date = datetime.date.today() - datetime.timedelta(days=1) form = RenewBookForm(data={'renewal_date': date}) self.assertFalse(form.is_valid()) def test_renew_form_date_too_far_in_future(self): date = datetime.date.today() + datetime.timedelta(weeks=4) + datetime.timedelta(days=1) form = RenewBookForm(data={'renewal_date': date}) self.assertFalse(form.is_valid()) def test_renew_form_date_today(self): date = datetime.date.today() form = RenewBookForm(data={'renewal_date': date}) self.assertTrue(form.is_valid()) def test_renew_form_date_max(self): date = timezone.localtime() + datetime.timedelta(weeks=4) form = RenewBookForm(data={'renewal_date': date}) self.assertTrue(form.is_valid())
تختبر الدالتان الأوليتان أن تسمية label
ونص التعليمات help_text
الخاصين بالحقل كما هو متوقع. ينبغي الوصول إلى الحقل باستخدام قاموس الحقول، مثل form.fields['renewal_date']
. لاحظ أنه ينبغي اختبار ما إذا كانت قيمة التسمية هي None
، لأن جانغو يعيد القيمة None
إن لم تُضبَط قيمة التسمية صراحةً بالرغم من أنه سيعرض التسمية الصحيحة.
تختبر بقية الدوال أن الاستمارة صالحة لتواريخ التجديد ضمن المجال المقبول وغير صالحة للقيم الموجودة خارج المجال. لاحظ كيفية بناء قيم تاريخ الاختبار حول تاريخنا الحالي datetime.date.today()
باستخدام datetime.timedelta()
(تحديد عدد الأيام أو الأسابيع في هذه الحالة)، ثم ننشئ الاستمارة فقط، ونمرر بياناتنا، ونختبر فيما إذا كانت صالحة أم لا.
ملاحظة: لم نستخدم هنا قاعدة البيانات أو عميل الاختبار فعليًا، لذا ضع في بالك تعديل هذه الاختبارات لاستخدام الصنف SimpleTestCase. يجب أيضًا التحقق من ظهور الأخطاء الصحيحة إذا كانت الاستمارة غير صالحة، ولكن ذلك يحدث عادةً بوصفه جزءًا من معالجة العرض، لذا سنعالج ذلك في القسم التالي.
تحذير: إذا استخدمتَ صنف ModelForm الذي هو RenewBookModelForm(forms.ModelForm)
بدلًا من الصنف RenewBookForm(forms.Form)
، فسيكون اسم حقل الاستمارة "due_back" بدلًا من "renewal_date".
هذا كل ما لدينا للتعامل مع الاستمارات وهناك بعض الأمور الأخرى، ولكن تنشِئها عروض التعديل المُعمَّمة المستندة إلى الأصناف Generic Class-based Editing Views تلقائيًا، ويجب اختبارها هناك، لذا يمكنك إجراء الاختبارات والتأكد من أن شيفرتك البرمجية لا تزال تعمل بنجاح.
العروض Views
يمكن التحقق من صحة سلوك العرض من خلال استخدام الصنف Client
لاختبار جانغو، إذ يعمل هذا الصنف بوصفه متصفح ويب وهمي dummy web browser يمكننا استخدامه لمحاكاة طلبات GET
و POST
على عنوان URL ومراقبة الاستجابة. يمكننا أن نرى كل شيء متعلق بالاستجابة تقريبًا، بدءًا من HTTP منخفض المستوى، أي ترويسات النتائج ورموز الحالة، إلى القالب الذي نستخدمه لعرض صفحة HTML وبيانات السياق التي نمررها إليه، ويمكننا رؤية سلسلة عمليات إعادة التوجيه (إن وجدت) والتحقق من عنوان URL ورمز الحالة في كل خطوة، مما يسمح لنا بالتحقق من أن كل عرض يطبّق المتوقع منه.
لنبدأ بواحدٍ من أبسط العروض، والذي يوفّر قائمة بجميع المؤلفين، ويُعرَض على عنوان URL، الذي هو "/catalog/authors/" (عنوان URL الذي اسمه "authors" في ضبط عناوين URL):
class AuthorListView(generic.ListView): model = Author paginate_by = 10
ينفِّذ جانغو كل شيء تقريبًا نيابةً عنا لأن هذا العرض هو عرض قائمة مُعمَّمة. يمكن القول أنه إذا كنت واثقًا في جانغو، فالشيء الوحيد الذي يجب اختباره هو أن العرض يمكن الوصول إليه عبر عنوان URL الصحيح ويمكن الوصول إليه باستخدام اسمه، لكن إذا استخدمتَ عملية تطوير مُقادة بالاختبارات، فستكتب اختبارات تؤكد أن العرض يظهر جميع المؤلفين، مع ترقيمهم ضمن مجموعات مؤلفة من 10 عناصر.
افتح الملف ""/catalog/tests/test_views.py وضع فيه شيفرة الاختبار التالية للصنف AuthorListView
بدلًا من أيّ نص آخر موجود فيه، إذ سنستورد نموذجنا وبعض الأصناف المفيدة، ونضبط في التابع setUpTestData()
عددًا من كائنات Author
لنتمكّن من اختبار ترقيم الصفحات:
from django.test import TestCase from django.urls import reverse from catalog.models import Author class AuthorListViewTest(TestCase): @classmethod def setUpTestData(cls): # إنشاء 13 مؤلفًا لاختبارات ترقيم الصفحات number_of_authors = 13 for author_id in range(number_of_authors): Author.objects.create( first_name=f'Dominique {author_id}', last_name=f'Surname {author_id}', ) def test_view_url_exists_at_desired_location(self): response = self.client.get('/catalog/authors/') self.assertEqual(response.status_code, 200) def test_view_url_accessible_by_name(self): response = self.client.get(reverse('authors')) self.assertEqual(response.status_code, 200) def test_view_uses_correct_template(self): response = self.client.get(reverse('authors')) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'catalog/author_list.html') def test_pagination_is_ten(self): response = self.client.get(reverse('authors')) self.assertEqual(response.status_code, 200) self.assertTrue('is_paginated' in response.context) self.assertTrue(response.context['is_paginated'] == True) self.assertEqual(len(response.context['author_list']), 10) def test_lists_all_authors(self): # الحصول على الصفحة الثانية والتأكد من أنها تحتوي (بالضبط) على 3 عناصر متبقية response = self.client.get(reverse('authors')+'?page=2') self.assertEqual(response.status_code, 200) self.assertTrue('is_paginated' in response.context) self.assertTrue(response.context['is_paginated'] == True) self.assertEqual(len(response.context['author_list']), 3)
تستخدم جميع الاختبارات العميل، الذي ينتمي إلى الصنف المشتق من TestCase
لمحاكاة طلب GET
والحصول على استجابة. تتحقق النسخة الأولى من عنوان URL المحدد (لاحظ وجود المسار المحدد فقط بدون النطاق)، بينما تولّد النسخة الثانية عنوان URL من اسمه في ضبط عناوين URL كما يلي:
response = self.client.get('/catalog/authors/') response = self.client.get(reverse('authors'))
نستعلم عن الاستجابة بعد الحصول عليها لنحصل على رمز حالتها والقالب المُستخدَم وما إذا كانت الاستجابة مرقمة paginated أم لا وعدد العناصر المُعادة وعدد العناصر الإجمالي.
ملاحظة: إذا ضبطتَ المتغير paginate_by
في الملف /catalog/views.py إلى عدد آخر غير العدد 10، فتأكد من تحديث الأسطر التي تختبر عرضَ عدد العناصر الصحيح في القوالب ذات الصفحات المرقمة السابقة وفي الأقسام التالية، فمثلًا إذا ضبطتَ متغيرًا لصفحة قائمة المؤلفين على القيمة 5، فعدّل السطر السابق إلى ما يلي:
self.assertTrue(len(response.context['author_list']) == 5)
المتغير الأهم الذي عرضناه سابقًا هو response.context
، وهو متغير السياق الذي يمرّره العرض إلى القالب، ويُعَد هذا المتغير مفيدًا للاختبار، لأنه يسمح لنا بالتأكد من أن قالبنا يحصل على جميع البيانات التي يحتاجها، أي يمكننا التحقق من أننا نستخدم القالب المقصود والبيانات التي يحصل عليها القالب، مما يؤدي إلى التحقق من رجوع أي مشكلات في التصيير rendering إلى القالب فقط.
العروض المقيدة على المستخدمين الذين سجلوا الدخول
سترغب في بعض الحالات في اختبار العرض المقتصر على المستخدمين الذين سجّلوا الدخول فقط، فالعرض LoanedBooksByUserListView
مثلًا مشابه جدًا لعرضنا السابق، ولكنه متاحٌ فقط للمستخدمين الذين سجّلوا الدخول، ويعرض فقط سجلات نسخ الكتاب BookInstance
التي استعارها المستخدم الحالي والتي لها الحالة 'on loan' ومرتبة بحسب الأقدم أولًا "oldest first".
from django.contrib.auth.mixins import LoginRequiredMixin class LoanedBooksByUserListView(LoginRequiredMixin, generic.ListView): """Generic class-based view listing books on loan to current user.""" model = BookInstance template_name ='catalog/bookinstance_list_borrowed_user.html' paginate_by = 10 def get_queryset(self): return BookInstance.objects.filter(borrower=self.request.user).filter(status__exact='o').order_by('due_back')
ضِف شيفرة الاختبار الآتية إلى الملف "/catalog/tests/test_views.py"، إذ نستخدم هنا أولًا التابع SetUp()
لإنشاء بعض حسابات تسجيل دخول المستخدم وكائنات BookInstance
-مع الكتب المرتبطة بها والسجلات الأخرى- التي سنستخدمها لاحقًا في الاختبارات. يستعير كل مستخدم تجريبي نصف الكتب، ولكننا ضبطنا في البداية حالة جميع الكتب على أنها قيد الصيانة "maintenance"، واستخدمنا التابع SetUp()
بدلًا من setUpTestData()
لأننا سنعدّل بعض هذه الكائنات لاحقًا.
ملاحظة: تنشئ شيفرة setUp()
التالية كتابًا بلغة Language
محددة، ولكن يمكن ألّا تحتوي شيفرتك البرمجية على النموذج Language
الذي تركناه بمثابة تحدٍ لك، لذا يمكنك تعليق أجزاء الشيفرة البرمجية التي تنشئ أو تستورد كائنات Language
، ويجب أيضًا تطبيق ذلك في قسم RenewBookInstancesViewTest
.
import datetime from django.utils import timezone from django.contrib.auth.models import User # مطلوب لضبط المستخدم بوصفه مستعيرًا from catalog.models import BookInstance, Book, Genre, Language class LoanedBookInstancesByUserListViewTest(TestCase): def setUp(self): # أنشئ مستخدمَين test_user1 = User.objects.create_user(username='testuser1', password='1X<ISRUkw+tuK') test_user2 = User.objects.create_user(username='testuser2', password='2HJ1vRV0Z&3iD') test_user1.save() test_user2.save() # أنشئ كتابًا test_author = Author.objects.create(first_name='John', last_name='Smith') test_genre = Genre.objects.create(name='Fantasy') test_language = Language.objects.create(name='English') test_book = Book.objects.create( title='Book Title', summary='My book summary', isbn='ABCDEFG', author=test_author, language=test_language, ) # أنشئ نوع الكتاب genre لخطوة لاحقة genre_objects_for_book = Genre.objects.all() # الإسناد المباشر لأنواع متعدد إلى متعدد غير مسموح به test_book.genre.set(genre_objects_for_book) test_book.save() # أنشئ 30 كائن BookInstance number_of_book_copies = 30 for book_copy in range(number_of_book_copies): return_date = timezone.localtime() + datetime.timedelta(days=book_copy%5) the_borrower = test_user1 if book_copy % 2 else test_user2 status = 'm' BookInstance.objects.create( book=test_book, imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=the_borrower, status=status, ) def test_redirect_if_not_logged_in(self): response = self.client.get(reverse('my-borrowed')) self.assertRedirects(response, '/accounts/login/?next=/catalog/mybooks/') def test_logged_in_uses_correct_template(self): login = self.client.login(username='testuser1', password='1X<ISRUkw+tuK') response = self.client.get(reverse('my-borrowed')) # تحقق من أن المستخدم قد سجل الدخول self.assertEqual(str(response.context['user']), 'testuser1') # تحقق من حصولنا على استجابة تمثل "النجاح" self.assertEqual(response.status_code, 200) # تحقق من استخدامنا للقالب الصحيح self.assertTemplateUsed(response, 'catalog/bookinstance_list_borrowed_user.html')
يمكن التحقق من أن العرض سيعيد توجيه المستخدم إلى صفحة تسجيل الدخول إن لم يسجّل دخول من خلال استخدام assertRedirects
كما هو موضح في test_redirect_if_not_logged_in()
. يمكن التحقق من إظهار الصفحة لمستخدم سجّل الدخول من خلال تسجيل الدخول للمستخدم التجريبي أولًا، ثم الوصول إلى الصفحة مرةً أخرى والتحقق من حصولنا على رمز الحالة status_code
التي هي 200 وتمثل النجاح.
تتحقق بقية الاختبارات من أن عروضنا تعيد فقط الكتب المُعارة للمستعير الحالي. انسخ الشيفرة البرمجية التالية والصقها في نهاية صنف الاختبار السابق:
def test_only_borrowed_books_in_list(self): login = self.client.login(username='testuser1', password='1X<ISRUkw+tuK') response = self.client.get(reverse('my-borrowed')) # تحقق من أن المستخدم قد سجّل الدخول self.assertEqual(str(response.context['user']), 'testuser1') # تحقق من حصولنا على استجابة تمثل "النجاح" self.assertEqual(response.status_code, 200) # تحقق من عدم وجود أيّ كتب في القائمة في البداية (لا توجد كتب مُعارة) self.assertTrue('bookinstance_list' in response.context) self.assertEqual(len(response.context['bookinstance_list']), 0) # غيّر الآن جميع الكتب لتكون مُعارة books = BookInstance.objects.all()[:10] for book in books: book.status = 'o' book.save() # تحقق من أننا قد استعرنا الكتب في القائمة response = self.client.get(reverse('my-borrowed')) # تحقق من أن المستخدم قد سجّل الدخول self.assertEqual(str(response.context['user']), 'testuser1') # تحقق من حصولنا على استجابة تمثل "النجاح" self.assertEqual(response.status_code, 200) self.assertTrue('bookinstance_list' in response.context) # تأكد من أن جميع الكتب تعود إلى المستخدم testuser1 وأنها مُعارة for bookitem in response.context['bookinstance_list']: self.assertEqual(response.context['user'], bookitem.borrower) self.assertEqual(bookitem.status, 'o') def test_pages_ordered_by_due_date(self): # غيّر جميع الكتب لتكون مُعارة for book in BookInstance.objects.all(): book.status='o' book.save() login = self.client.login(username='testuser1', password='1X<ISRUkw+tuK') response = self.client.get(reverse('my-borrowed')) # تحقق من أن المستخدم قد سجّل الدخول self.assertEqual(str(response.context['user']), 'testuser1') # تحقق من حصولنا على استجابة تمثل "النجاح" self.assertEqual(response.status_code, 200) # تأكد من عرض 10 عناصر فقط بسبب ترقيم الصفحات self.assertEqual(len(response.context['bookinstance_list']), 10) last_date = 0 for book in response.context['bookinstance_list']: if last_date == 0: last_date = book.due_back else: self.assertTrue(last_date <= book.due_back) last_date = book.due_back
يمكنك أيضًا إضافة اختبارات لترقيم الصفحات Pagination إذا أردتَ ذلك.
اختبار العروض مع الاستمارات
يُعَد اختبار العرض مع الاستمارات أكثر تعقيدًا من الحالات السابقة، لأنه يجب اختبار مزيدٍ من مسارات الشيفرات البرمجية وهي: الإظهار الأولي، والإظهار بعد فشل التحقق من صحة البيانات، والإظهار بعد نجاح التحقق من صحة البيانات. والخبر السار هو أننا نستخدم العميل للاختبار باستخدام الطريقة نفسها التي استخدمناها لاختبار العروض المتاحة للعرض فقط.
لنكتب بعض اختبارات العرض المُستخدَم لتجديد الكتب renew_book_librarian()
:
from catalog.forms import RenewBookForm @permission_required('catalog.can_mark_returned') def renew_book_librarian(request, pk): """View function for renewing a specific BookInstance by librarian.""" book_instance = get_object_or_404(BookInstance, pk=pk) # إذا كان هذا الطلب من النوع POST، فعالج بيانات الاستمارة if request.method == 'POST': # أنشئ نسخة من الاستمارة واملأها ببيانات من الطلب (الربط Binding): book_renewal_form = RenewBookForm(request.POST) # تحقق من أن الاستمارة صالحة if form.is_valid(): # معالجة البيانات في form.cleaned_data كما هو مطلوب (نكتبها هنا فقط في حقل النموذج due_back) book_instance.due_back = form.cleaned_data['renewal_date'] book_instance.save() # إعادة التوجيه إلى عنوان URL جديد return HttpResponseRedirect(reverse('all-borrowed')) # إذا كان تابع GET (أو أي تابع آخر)، فأنشئ الاستمارة الافتراضية else: proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3) book_renewal_form = RenewBookForm(initial={'renewal_date': proposed_renewal_date}) context = { 'book_renewal_form': book_renewal_form, 'book_instance': book_instance, } return render(request, 'catalog/book_renew_librarian.html', context)
ينبغي اختبار أن العرض متاح فقط للمستخدمين الذين لديهم الإذن can_mark_returned
، ويُعاد توجيه المستخدمين إلى صفحة خطأ HTTP 404 إذا حاولوا تجديد نسخة كتاب BookInstance
ليست موجودة. ينبغي أن نتحقق من ضبط القيمة الأولية للاستمارة بتاريخ لثلاثة أسابيع في المستقبل، وأنه إذا نجحت عملية التحقق من صحة البيانات، فسيُعاد توجيهنا إلى عرض "جميع الكتب المستعارة"، وسنتحقق -كجزء من فحص اختبارات فشل التحقق من صحة البيانات- من أن استمارتنا ترسل رسائل الخطأ المناسبة.
أضِف الجزء الأول من صنف الاختبار الموضح فيما يلي إلى نهاية الملف "/catalog/tests/test_views.py"، إذ يؤدي ذلك إلى إنشاء مستخدمَين ونسختين من الكتاب، ولكنه يمنح مستخدمًا واحدًا فقط الإذن المطلوب للوصول إلى العرض:
import uuid from django.contrib.auth.models import Permission # مطلوب لمنح الإذن اللازم لضبط كتاب على أنه مُعاد class RenewBookInstancesViewTest(TestCase): def setUp(self): # أنشئ مستخدمًا test_user1 = User.objects.create_user(username='testuser1', password='1X<ISRUkw+tuK') test_user2 = User.objects.create_user(username='testuser2', password='2HJ1vRV0Z&3iD') test_user1.save() test_user2.save() # امنح المستخدم test_user2 الإذن لتجديد الكتب permission = Permission.objects.get(name='Set book as returned') test_user2.user_permissions.add(permission) test_user2.save() # أنشئ كتابًا test_author = Author.objects.create(first_name='John', last_name='Smith') test_genre = Genre.objects.create(name='Fantasy') test_language = Language.objects.create(name='English') test_book = Book.objects.create( title='Book Title', summary='My book summary', isbn='ABCDEFG', author=test_author, language=test_language, ) # أنشئ نوع الكتاب genre لخطوة لاحقة genre_objects_for_book = Genre.objects.all() # الإسناد المباشر لأنواع متعدد إلى متعدد غير مسموح به test_book.genre.set(genre_objects_for_book) test_book.save() # أنشئ كائن BookInstance للمستخدم test_user1 return_date = datetime.date.today() + datetime.timedelta(days=5) self.test_bookinstance1 = BookInstance.objects.create( book=test_book, imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user1, status='o', ) # أنشئ كائن BookInstance للمستخدم test_user2 return_date = datetime.date.today() + datetime.timedelta(days=5) self.test_bookinstance2 = BookInstance.objects.create( book=test_book, imprint='Unlikely Imprint, 2016', due_back=return_date, borrower=test_user2, status='o', )
أضِف الاختبارات التالية إلى نهاية صنف الاختبار، إذ تتحقق هذه الاختبارات من أن المستخدمين الذين لديهم الأذونات الصحيحة فقط (المستخدم testuser2) يمكنهم الوصول إلى العرض. سنتحقق من جميع الحالات وهي:
- عندما لا يسجّل المستخدم الدخول.
- عندما يسجّل المستخدم الدخول ولكن ليس لديه الأذونات الصحيحة.
- عندما يكون لدى المستخدم أذونات ولكنه ليس المستعير (يجب أن تنجح هذه الحالة).
كما سنتحقق مما سيحدث عندما يحاولون الوصول إلى نسخة كتاب BookInstance
غير موجودة، وكذلك من استخدام القالب الصحيح.
def test_redirect_if_not_logged_in(self): response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk})) # تحقق يدويًا من إعادة التوجيه # لا يمكن استخدام assertRedirect، لأن عنوان URL لإعادة التوجيه لا يمكن التنبؤ به self.assertEqual(response.status_code, 302) self.assertTrue(response.url.startswith('/accounts/login/')) def test_forbidden_if_logged_in_but_not_correct_permission(self): login = self.client.login(username='testuser1', password='1X<ISRUkw+tuK') response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk})) self.assertEqual(response.status_code, 403) def test_logged_in_with_permission_borrowed_book(self): login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD') response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance2.pk})) # تأكد من أنه يتيح لنا تسجيل الدخول، فهذا كتابنا ولدينا الأذونات الصحيحة self.assertEqual(response.status_code, 200) def test_logged_in_with_permission_another_users_borrowed_book(self): login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD') response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk})) # تحقق من أنه يتيح لنا تسجيل الدخول، لأننا أمناء مكتبة، ويمكننا عرض جميع كتب المستخدمين self.assertEqual(response.status_code, 200) def test_HTTP404_for_invalid_book_if_logged_in(self): # معرّف uid لمطابقة نسخة كتابنا- غير مرجح حدوثه test_uid = uuid.uuid4() login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD') response = self.client.get(reverse('renew-book-librarian', kwargs={'pk':test_uid})) self.assertEqual(response.status_code, 404) def test_uses_correct_template(self): login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD') response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk})) self.assertEqual(response.status_code, 200) # تحقق من أننا نستخدم القالب الصحيح self.assertTemplateUsed(response, 'catalog/book_renew_librarian.html')
أضِف تابع الاختبار التالي الذي يتحقق من أن التاريخ الأولي للاستمارة هو ثلاثة أسابيع في المستقبل، ولاحظ كيف أننا قادرون على الوصول إلى القيمة الأولية لحقل الاستمارة response.context['form'].initial['renewal_date']
:
def test_form_renewal_date_initially_has_date_three_weeks_in_future(self): login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD') response = self.client.get(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk})) self.assertEqual(response.status_code, 200) date_3_weeks_in_future = datetime.date.today() + datetime.timedelta(weeks=3) self.assertEqual(response.context['form'].initial['renewal_date'], date_3_weeks_in_future)
يتحقق الاختبار التالي (أضفه إلى الصنف أيضًا) من أن العرض يعيد توجيهك إلى قائمة جميع الكتب المستعارة إذا نجح التجديد، والاختلاف هنا هو أننا لأول مرة نعرض كيفية إرسال البيانات POST
باستخدام العميل، بحيث تكون هذه البيانات هي الوسيط الثاني للدالة post
، وتُحدَّد بوصفها قاموسًا من أزواج مفتاح/قيمة.
def test_redirects_to_all_borrowed_book_list_on_success(self): login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD') valid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=2) response = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future}) self.assertRedirects(response, reverse('all-borrowed'))
تحذير: أُضيف العرض all-borrowed كتحدٍ لك، ويمكن أن تعيد شيفرتك البرمجية التوجيه إلى الصفحة الرئيسية '/' بدلًا من ذلك، فإذا كان الأمر كذلك، فعدّل آخر سطرين من شيفرة الاختبار لتكون مماثلة للشيفرة البرمجية التالية، إذ يضمن الوسيط follow=True
في الطلب أن يعيد الطلب عنوان URL للهدف النهائي وعندها يجب تحديد /catalog/
بدلًا من /
:
response = self.client.post(reverse('renew-book-librarian', kwargs={'pk':self.test_bookinstance1.pk,}), {'renewal_date':valid_date_in_future}, follow=True) self.assertRedirects(response, '/catalog/')
انسخ الدالتين التاليتين في الصنف، واللتين تختبران طلبات POST
مرةً ثانية، ولكن مع تواريخ تجديد غير صالحة في هذه الحالة. سنستخدم الدالة assertFormError()
للتحقق من أن رسائل الخطأ كما هو متوقع:
def test_form_invalid_renewal_date_past(self): login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD') date_in_past = datetime.date.today() - datetime.timedelta(weeks=1) response = self.client.post(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk}), {'renewal_date': date_in_past}) self.assertEqual(response.status_code, 200) self.assertFormError(response, 'form', 'renewal_date', 'Invalid date - renewal in past') def test_form_invalid_renewal_date_future(self): login = self.client.login(username='testuser2', password='2HJ1vRV0Z&3iD') invalid_date_in_future = datetime.date.today() + datetime.timedelta(weeks=5) response = self.client.post(reverse('renew-book-librarian', kwargs={'pk': self.test_bookinstance1.pk}), {'renewal_date': invalid_date_in_future}) self.assertEqual(response.status_code, 200) self.assertFormError(response, 'form', 'renewal_date', 'Invalid date - renewal more than 4 weeks ahead')
يمكن استخدام الأساليب نفسها لاختبار العرض الآخر.
القوالب
يوفر جانغو واجهات برمجة التطبيقات API خاصة بالاختبار للتحقق من أن عرضك يستدعي القالب الصحيح، وللسماح بالتحقق من إرسال المعلومات الصحيحة، ولكن لا يوجد دعم محدد لواجهة برمجة التطبيقات للاختبار في جانغو بحيث يُظهر خرج صفحة HTML كما هو متوقع.
أدوات الاختبار الأخرى الموصى بها
يمكن أن يساعدك إطار عمل اختبار جانغو في كتابة اختبارات الوحدة والتكامل الفعالة، إذ تعرّفنا فقط على جزء بسيط مما يمكن أن يفعله إطار عمل unittest الأساسي، بالإضافة إلى إضافات جانغو. اطلّع مثلًا على كيفية استخدام unittest.mock لتعويض نقص المكتبات الخارجية لتتمكّن من اختبار شيفرتك البرمجية بدقة أكبر.
هناك العديد من أدوات الاختبار الأخرى التي يمكنك استخدامها، ولكننا سنذكر أداتين هما:
- Coverage: تعطي هذه الأداة الخاصة ببايثون تقريرًا بمقدار شيفرتك البرمجية التي تنّفذها اختباراتك، وهي مفيدة خاصةً عندما تبدأ وتحاول تحديد ما يجب عليك اختباره بالضبط.
- إطار عمل سيلينيوم Selenium: وهو إطار عمل لأتمتة الاختبار في متصفح حقيقي، ويسمح بمحاكاة تفاعل مستخدم حقيقي مع الموقع، ويوفر إطار عمل رائع لنظام اختبار موقعك (الخطوة التالية من اختبار التكامل).
تحدى نفسك
هناك الكثير من النماذج والعروض التي يمكننا اختبارها، لذا حاول إنشاء حالة اختبار للعرض AuthorCreate
:
class AuthorCreate(PermissionRequiredMixin, CreateView): model = Author fields = '__all__' initial = {'date_of_death':'12/10/2016'} permission_required = 'catalog.can_mark_returned'
تذكّر أنه يجب التحقق من أيّ شيء تحدده أو أيّ شيء يمثل جزءًا من التصميم، وسيشمل ذلك مَن يمكنه الوصول والتاريخ الابتدائي والقالب المُستخدم والمكان الذي يعيد العرض التوجيه إليه عند النجاح.
الخلاصة
لا تُعَد كتابة شيفرة الاختبار ممتعة، وتُترَك غالبًا إلى النهاية، أو لا تُطبَّق إطلاقًا عند إنشاء موقع ويب، لكنها جزءٌ أساسي من التأكد من أن شيفرتك البرمجية آمنة لإصدارها بعد إجراء التغييرات وصيانتها بفعالية من حيث التكلفة.
أوضحنا في هذا المقال كيفية كتابة وتشغيل اختبارات النماذج والاستمارات والعروض، وقدّمنا ملخصًا موجزًا لما يجب عليك اختباره، والذي يكون غالبًا أصعب شيء يجب تطبيقه في البداية. أمامك الكثير لتعلّمه، ولكن يجب أن تكون قادرًا على إنشاء اختبارات وحدة فعّالة لمواقعك حتى مع كل ما تعلمته مسبقًا.
سنوضح في المقال التالي كيفية نشر موقع جانغو الذي اختبَرناه بالكامل.
ترجمة -وبتصرُّف- للمقال Django Tutorial Part 10: Testing a Django web application.
اقرأ المزيد
- المقال التالي: تطبيق عملي لتعلم جانغو - الجزء 10: نشر تطبيق جانغو في بيئة الإنتاج
- المقال السابق تطبيق عملي لتعلم جانغو - الجزء الثامن: العمل مع الاستمارات Forms
- كتابة وفحص الاختبارات (توثيق جانغو)
- كتابة أول تطبيق جانغو - الجزء الخامس: كتابة اختبار آلي (توثيق جانغو)
- أدوات الفحص (توثيق جانغو)
- مدخل إلى اختبارات مشاريع الويب الآلية للتوافق مع المتصفحات
- الدليل السريع إلى لغة البرمجة بايثون Python
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.