يُعد Django REST Framework، والذي يُختصر عادةً إلى DRF، مجموعة الأدوات القياسية لبناء واجهات برمجة التطبيقات (APIs) فوق Django. يوفر لك هذا الإطار أدوات Serializers وViewsets وRouters وطبقة مصادقة. وما يوفره أيضًا، والذي يقل استخدام العديد من المطورين له، هو طبقة اختبار قوية تستند إلى مشغل الاختبار الخاص بـ Django.
يوضح هذا الدليل كيفية اختبار واجهة برمجة تطبيقات DRF بطريقتين. أولاً، الاختبارات التلقائية المكتوبة بلغة بايثون باستخدام APITestCase و APIClient الخاصة بـ DRF، والتي تعمل بدون خادم مباشر وتكتشف الأخطاء في كل عملية commit. ثانيًا، اختبار نقاط النهاية قيد التشغيل باستخدام عميل واجهة برمجة تطبيقات، وهي الطريقة التي تستكشف بها السلوك وتتحقق من الخدمة الحقيقية. كلاهما مهم، وكل منهما يكتشف مشاكل مختلفة.
إعداد المشروع وبيئة الاختبار
ابدأ ببيئة معزولة بحيث تظل تبعيات الاختبار منفصلة عن بقية نظامك:
python -m venv venv
source venv/bin/activate
pip install django djangorestframework coverage
يتم اختبار مشروع DRF مثل أي مشروع Django آخر. حسب الاصطلاح، توجد الاختبارات في ملف tests.py داخل كل تطبيق، أو في حزمة tests/ إذا كان لديك العديد منها. يكتشف مشغل اختبار Django أي فئة ترث من نوع TestCase وأي دالة يبدأ اسمها بـ test_.
الخيار الأساسي هو تحديد الفئة الأساسية التي سيتم استخدامها. لا تحتوي unittest.TestCase العادية على قاعدة بيانات. تقوم TestCase الخاصة بـ Django بتغليف كل اختبار في معاملة (transaction) وتتراجع عنها، بحيث لا تؤثر الاختبارات على بعضها البعض. تمتد APITestCase الخاصة بـ DRF من TestCase الخاصة بـ Django وتستبدل العميل بـ APIClient الخاص بـ DRF، والذي يفهم آليات المصادقة وأنواع المحتوى في DRF. لعمل واجهة برمجة التطبيقات، استخدم APITestCase.
هناك فئة أخرى تستحق المعرفة. لا تقوم TransactionTestCase بتغليف الاختبارات في معاملة، وهو ما تحتاجه عندما يدير الكود قيد الاختبار المعاملات بنفسه أو يعتمد على ميزات قاعدة البيانات التي قد تخفيها معاملة تغليفية. إنها أبطأ لأنها تقطع الجداول بين الاختبارات بدلاً من التراجع، لذا استخدمها فقط عندما لا تستطيع TestCase محاكاة السلوك بشكل حقيقي. بالنسبة للغالبية العظمى من اختبارات نقاط النهاية، تعد APITestCase الخيار الصحيح والأسرع.
من المفيد أيضًا التفكير في بيانات الاختبار مبكرًا. استخدم دالة setUp لإنشاء الصفوف التي يحتاجها كل اختبار في الفئة، أو setUpTestData إذا كانت البيانات للقراءة فقط ويمكن مشاركتها عبر الفئة لزيادة السرعة. بالنسبة للمشاريع الكبيرة، تقوم مكتبة مصنع مثل factory_boy بإنشاء مثيلات نماذج صالحة دون الحاجة إلى تحديد كل حقل، مما يحافظ على الاختبارات قصيرة ومرنة عندما يكتسب النموذج حقلًا مطلوبًا جديدًا.
اختبار أدوات Serializers كوحدات
تقوم أدوات Serializers بالتحقق من الصحة والتحويل بين مثيلات النموذج وJSON. إنها صغيرة ونقية، مما يجعلها مثالية لاختبارات الوحدة السريعة التي لا تتصل بنقاط النهاية.
لنفترض أن لديك نموذج Article و ArticleSerializer. يتحقق اختبار الوحدة من أن البيانات الصالحة تمر والبيانات غير الصالحة تفشل:
from django.test import TestCase
from articles.serializers import ArticleSerializer
class ArticleSerializerTests(TestCase):
def test_valid_data_passes(self):
data = {"title": "Caching strategies", "body": "Use ETags."}
serializer = ArticleSerializer(data=data)
self.assertTrue(serializer.is_valid())
def test_missing_title_fails(self):
data = {"body": "No title here."}
serializer = ArticleSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn("title", serializer.errors)
تعمل هذه الاختبارات في غضون أجزاء من الثانية لأنه لا يوجد HTTP ولا عرض (view). تخبرك أن قواعد التحقق من الصحة صحيحة حتى قبل أن تفكر في نقطة النهاية. هذه التغطية على مستوى الوحدة هي أساس هرم الاختبار الموضح في ما هو الاختبار التلقائي.
اختبار نقاط النهاية باستخدام APITestCase و APIClient
تتحقق اختبارات نقاط النهاية من المسار الكامل: التوجيه (routing)، والعرض (view)، والمُسرِّل (serializer)، والاستجابة. يرسل APIClient الخاص بـ DRF الطلبات داخل العملية، لذلك لا يتم تشغيل خادم وتبقى الاختبارات سريعة.
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from articles.models import Article
class ArticleEndpointTests(APITestCase):
def setUp(self):
Article.objects.create(title="First post", body="Hello world")
def test_list_articles_returns_200(self):
url = reverse("article-list")
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
def test_create_article(self):
url = reverse("article-list")
payload = {"title": "Second post", "body": "More content"}
response = self.client.post(url, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Article.objects.count(), 2)
بعض التفاصيل الجديرة بالذكر. self.client هو مثيل لـ APIClient يوفره APITestCase. استخدم reverse() مع اسم المسار بدلاً من ترميز عناوين URL الثابتة، حتى لا يؤدي تغيير المسار إلى تعطيل كل اختبار. مرر format="json" عند عمليات الكتابة ليقوم العميل بتحويل البيانات المرسلة (payload) بشكل صحيح. تحقق من رمز الحالة باستخدام الثوابت المسماة من rest_framework.status، حيث إنها تُقرأ بوضوح أكبر من الأرقام الخام. يتم تغطية رموز الحالة نفسها في الدليل حول رموز حالة HTTP التي يجب أن تستخدمها واجهات برمجة تطبيقات REST.
اختبار المصادقة والأذونات
تحمي معظم واجهات برمجة التطبيقات الحقيقية نقاط نهايتها. تحتاج إلى اختبارات تثبت أن الطلبات غير المصادق عليها يتم رفضها، وأن الطلبات المصادق عليها ذات الأذونات الصحيحة تنجح.
يوفر APIClient طريقتين لمصادقة الاختبار. تتجاوز force_authenticate() التحقق من بيانات الاعتماد وترفق مستخدمًا مباشرةً، وهو أمر مثالي لاختبار منطق العرض نفسه. تقوم login() أو credentials() بتشغيل مسار المصادقة الحقيقي. إليك اختبار إذن يستخدم كلا الاتجاهين:
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
from django.urls import reverse
class ArticlePermissionTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username="editor", password="testpass123"
)
self.url = reverse("article-list")
def test_anonymous_cannot_create(self):
payload = {"title": "Blocked", "body": "Should fail"}
response = self.client.post(self.url, payload, format="json")
self.assertEqual(
response.status_code, status.HTTP_403_FORBIDDEN
)
def test_authenticated_user_can_create(self):
self.client.force_authenticate(user=self.user)
payload = {"title": "Allowed", "body": "Should pass"}
response = self.client.post(self.url, payload, format="json")
self.assertEqual(
response.status_code, status.HTTP_201_CREATED
)
يؤكد الاختبار الأول أن فئة الإذن تمنع الكتابات المجهولة. ويؤكد الثاني أن المستخدم المصادق عليه يمر. معًا، يحددان حدود الإذن، وهو بالضبط نوع السلوك الذي ينكسر بصمت أثناء عمليات إعادة الهيكلة. إذا كنت تختبر مصادقة الرمز المميز (token auth) تحديدًا، فاستبدل force_authenticate بـ self.client.credentials(HTTP_AUTHORIZATION="Token " + token) لتشغيل مسار الرأس الحقيقي.
يجدر أن تكون متعمدًا بشأن أي طريقة تستخدمها. force_authenticate أسرع وتعزل منطق العرض، لذا فهي تناسب غالبية اختبارات الأذونات حيث تهتم فقط بما إذا كان الدور يمكنه فعل شيء أم لا. مسار بيانات الاعتماد الحقيقي هو ما تريده لمجموعة أصغر من الاختبارات التي تثبت تحديدًا أن آلية المصادقة تعمل: أن الرمز المميز السيء يتم رفضه، وأن الرمز المميز منتهي الصلاحية يفشل، وأن نقطة نهاية تسجيل الدخول تصدر رمزًا مميزًا قابلاً للاستخدام. يمنحك مزج كليهما تغطية واسعة بتكلفة منخفضة وتغطية عميقة حيثما يهم.
لا تنسَ أذونات مستوى الكائن (object-level permissions). تسمح العديد من واجهات برمجة تطبيقات DRF للمستخدم بتحرير سجلاته الخاصة ولكن ليس سجلات الآخرين. اختبر ذلك بشكل صريح: أنشئ مستخدمين، اجعل أحدهما ينشئ سجلًا، ثم تأكد من أن المستخدم الآخر يحصل على 403 أو 404 عند محاولة تعديله. تستحق نقاط نهاية القائمة نفس التدقيق، حيث يمكن لمجموعة استعلام (queryset) غير محكمة أن تعيد صفوفًا لا ينبغي للمستخدم رؤيتها أبدًا حتى عندما تكون نقطة نهاية التفاصيل مؤمنة.
تشغيل مجموعة الاختبار وقياس التغطية
شغل كل اختبار باستخدام مشغل Django:
python manage.py test
يكتشف المشغل فئات الاختبار الخاصة بك، ويقوم بإعداد قاعدة بيانات اختبار مؤقتة، ويشغل كل اختبار داخل معاملة، ويتراجع عنها بعد ذلك. تشغيل نظيف يطبع OK. ويُظهر الفشل الادعاء الذي تعطل ومكانه.
معرفة أن مجموعة الاختبار تعمل بنجاح ليس هو نفسه معرفة أنها تغطي ما يكفي. تقيس أداة coverage السطور التي تم تشغيلها فعليًا:
coverage run --source='.' manage.py test
coverage report
coverage html
يسرد التقرير كل ملف بنسبة السطور التي تم تنفيذها. يبرز إخراج HTML السطور غير المختبرة باللون الأحمر، مما يوجهك مباشرة إلى الثغرات. اهدف إلى تغطية ذات مغزى للعروض والمُسَرِّلات والأذونات بدلاً من رقم إجمالي واحد. تغطي المقالة حول كيفية كتابة سكربتات الاختبار التلقائية ما يجعل الاختبار يستحق الاحتفاظ به، حيث أن تغطية الاختبارات الضعيفة ليست أمانًا حقيقيًا.
اختبار واجهة برمجة التطبيقات المباشرة باستخدام عميل
تعد اختبارات بايثون التلقائية سريعة وتعمل في CI، لكنها تختبر التطبيق داخل العملية. لا تكتشف المشكلات التي تظهر فقط مقابل الخدمة قيد التشغيل: رأس CORS تم تكوينه بشكل خاطئ، أو وكيل عكسي (reverse proxy) يزيل شيئًا ما، أو قاعدة بيانات بطيئة تحت حمل حقيقي، أو اختلاف بين قاعدة بيانات الاختبار الخاصة بك والإنتاج. لذلك، ترسل طلبات حقيقية إلى نقاط النهاية المنشورة.
تعد Apidog مناسبة جدًا هنا. إنها منصة واجهة برمجة تطبيقات شاملة، لذا يمكنك استيراد مخطط OpenAPI الخاص بواجهة برمجة تطبيقات DRF الخاصة بك، وإرسال طلبات حية إلى خادم قيد التشغيل، وبناء تأكيدات مرئية دون كتابة المزيد من بايثون. يمكن لـ DRF إنشاء مخطط OpenAPI، ويستهلكه Apidog مباشرة، مما يحافظ على مزامنة عميلك مع العقد الفعلي. يمكنك أيضًا بناء سيناريوهات اختبار متعددة الخطوات، على سبيل المثال تسجيل الدخول، إنشاء مقالة، جلبها، حذفها، وتشغيلها على جدول زمني أو في CI. هذا يكمل مجموعة APITestCase الخاصة بك بدلاً من استبدالها: اختبارات الوحدة ونقاط النهاية تحمي الكود، وعميل واجهة برمجة التطبيقات يحمي الخدمة المنشورة. يمكنك تنزيل Apidog لاستيراد مخطط DRF وتجربته. للفرق التي تفضل البقاء في بايثون من البداية إلى النهاية، يوضح دليل إطار عمل اختبار API التلقائي pytest كيفية تشغيل اختبارات نمط DRF تحت pytest.
الأسئلة المتكررة
ما الفرق بين TestCase و APITestCase؟
تقوم TestCase في Django بتغليف كل اختبار في معاملة قاعدة بيانات وتوفر لك عميل اختبار Django قياسيًا. APITestCase في DRF ترث منها وتستبدل العميل بـ APIClient، الذي يفهم مخططات مصادقة DRF، وتفاوض المحتوى، ومعامل format في الطلبات. استخدم APITestCase لاختبار نقاط نهاية DRF.
متى يجب أن أستخدم force_authenticate بدلاً من login؟
استخدم force_authenticate() عندما ترغب في اختبار منطق العرض والأذونات دون تكلفة وتعقيد تدفق بيانات الاعتماد الحقيقي. يقوم بإرفاق مستخدم بالطلب مباشرة. استخدم login() أو credentials() عندما تكون آلية المصادقة نفسها، مثل مصادقة الجلسة أو الرمز المميز، هي ما تريد التحقق منه.
هل تحتاج اختبارات DRF إلى خادم قيد التشغيل؟
لا. يقوم APIClient بإرسال الطلبات داخل العملية مباشرة إلى عروضك، لذا تعمل مجموعة الاختبار دون بدء خادم. هذا ما يجعلها سريعة. لاختبار الخدمة المنشورة فعليًا، بما في ذلك البنية التحتية مثل الوكلاء (proxies) و CORS، ترسل طلبات HTTP حقيقية باستخدام عميل واجهة برمجة تطبيقات مثل Apidog.
كيف أتحقق من تغطية الاختبار لمشروع DRF؟
قم بتثبيت حزمة coverage، ثم قم بتشغيل coverage run --source='.' manage.py test متبوعًا بـ coverage report للحصول على ملخص أو coverage html للحصول على عرض سطر بسطر. يبرز تقرير HTML السطور غير المختبرة حتى تتمكن من رؤية العروض أو المُسَرِّلات التي تفتقر إلى الاختبارات.
هل يجب أن أختبر أدوات Serializers ونقاط النهاية بشكل منفصل؟
نعم. اختبارات وحدة Serializer سريعة وتحدد أخطاء التحقق من الصحة دون أي حمل إضافي لـ HTTP. تتحقق اختبارات نقطة النهاية باستخدام APITestCase من التوجيه والأذونات ودورة الطلب الكاملة. الاحتفاظ بكليهما يمنحك ملاحظات سريعة على مستوى الوحدة وثقة في أن التوصيلات تعمل على مستوى التكامل.
