يصل مطورو Python إلى pytest لأنه لا يعيق العمل. الاختبار هو مجرد دالة يبدأ اسمها بـ test_، والتأكيد هو عبارة assert بسيطة، ويقوم المشغل بالباقي. ادمجه مع مكتبة requests وستحصل على إطار عمل كامل يعتمد على الكود أولاً لأتمتة اختبارات API، بدون أي تعقيدات زائدة.
يعرض هذا البرنامج التعليمي كيفية بناء مجموعة اختبار API حقيقية باستخدام pytest. ستقوم بإعداد المشروع، وكتابة أول اختبار طلب لك، ومشاركة منطق الإعداد باستخدام الـ fixtures، وتشغيل نفس الاختبار مقابل العديد من المدخلات باستخدام parametrize، والتأكيد على حالة الاستجابة، وجسمها، ومخطط JSON الخاص بها. يستخدم كل مثال واجهة برمجة تطبيقات عامة واقعية حتى تتمكن من تكييف الكود مباشرة.
إعداد المشروع
قم بتثبيت المكتبتين اللتين تحتاجهما في بيئة افتراضية:
python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
يحافظ التخطيط النظيف على قابلية صيانة المجموعة مع نموها:
api-tests/
conftest.py # shared fixtures
test_users.py # tests for the users endpoints
test_orders.py # tests for the orders endpoints
pytest.ini # configuration
يكتشف Pytest الاختبارات تلقائيًا. يجب أن تبدأ الملفات بـ test_ أو تنتهي بـ _test.py، ويجب أن تبدأ الدوال بـ test_، ويجب أن تبدأ فئات الاختبار بـ Test ولا تحتوي على دالة __init__. اتبع هذه القواعد ولن تضطر أبدًا إلى تكوين الاكتشاف يدويًا. إذا كانت الاختبارات الآلية كمجال جديدة عليك، فإن دليلنا حول ما هي الاختبارات الآلية يحدد السياق.
لماذا pytest لاختبار API على وجه التحديد؟ تتولى مكتبة requests التعامل مع HTTP، ويتولى pytest كل شيء آخر: الاكتشاف، التأكيدات مع إخراج فشل قابل للقراءة، الإعداد والتفكيك من خلال الـ fixtures، التشغيل القائم على البيانات من خلال parametrize، والإبلاغ. تقوم بتجميع إطار عمل كامل لأتمتة API من مكتبتين صغيرتين وموثقتين جيدًا، بلغة من المرجح أن يستخدمها فريقك بالفعل للتطبيق نفسه. هذا القرب مهم. الاختبارات التي تعيش في نفس المستودع مثل الكود تبقى صادقة، لأن التغيير الذي يسبب كسرًا واختباره الفاشل يظهران في نفس طلب السحب.
كتابة أول اختبار API لك
يرسل اختبار API في pytest طلبًا ويؤكد على الاستجابة. إليك اختبار لنقطة نهاية المستخدمين:
import requests
BASE_URL = "https://api.example.com/v1"
def test_get_user_returns_200():
response = requests.get(f"{BASE_URL}/users/42")
assert response.status_code == 200
def test_get_user_returns_expected_fields():
response = requests.get(f"{BASE_URL}/users/42")
body = response.json()
assert body["id"] == 42
assert "email" in body
assert body["status"] == "active"
قم بتشغيل المجموعة باستخدام pytest -v. ينتج كل assert يفشل تقريرًا مفصلاً يوضح القيمة الفعلية، وهي إحدى أفضل ميزات pytest. لا تحتاج إلى طرق تأكيد خاصة؛ يعيد إطار العمل كتابة عبارات assert العادية لتعطي إخراجًا غنيًا. لمجموعة أوسع من الفحوصات التي تستحق إجراؤها على الاستجابة، راجع دليلنا حول تأكيدات API.
مشاركة الإعداد باستخدام الـ fixtures
تكرار عنوان URL الأساسي وجلسة HTTP في كل اختبار هو إهدار. الـ fixtures تحل هذه المشكلة. الـ fixture هي دالة مزينة بـ @pytest.fixture تنتج قيمة يمكن للاختبارات طلبها بتسميتها كمعامل.
ضع الـ fixtures المشتركة في conftest.py حتى تتمكن كل ملفات الاختبار من استخدامها دون استيراد:
# conftest.py
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.fixture(scope="session")
def api_session():
session = requests.Session()
session.headers.update({"Accept": "application/json"})
yield session
session.close()
@pytest.fixture
def auth_token(api_session):
response = api_session.post(
f"{BASE_URL}/auth/login",
json={"email": "qa@example.com", "password": "test-pass"},
)
return response.json()["token"]
يعني الوسيط scope="session" أن الجلسة يتم إنشاؤها مرة واحدة للتشغيل بأكمله بدلاً من كل اختبار. تقسم الكلمة المفتاحية yield الإعداد عن التفكيك: يتم تشغيل الكود قبل yield أولاً، ويتم تشغيل الكود بعد ذلك عندما يخرج الـ fixture عن النطاق. يطلب الاختبار ببساطة ما يحتاجه:
def test_create_order(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
assert response.status_code == 201
assert response.json()["status"] == "pending"
الـ Fixtures هي بديل pytest الحديث للأسلوب القديم setup_function و teardown_function. إنها تتكون بشكل نظيف، وتدعم النطاقات، وتجعل التبعيات صريحة، ولهذا السبب توصي وثائق pytest fixtures الرسمية بها كنهج افتراضي.
تشغيل اختبار واحد مقابل العديد من المدخلات
عادةً ما تحتاج نقاط نهاية API إلى الفحص مقابل العديد من المدخلات: القيم الصالحة، والقيم غير الصالحة، والحالات الحافة. كتابة دالة منفصلة لكل منها أمر ممل. ي decorator @pytest.mark.parametrize يقوم بتشغيل جسم اختبار واحد مقابل قائمة من المدخلات:
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.mark.parametrize("user_id,expected_status", [
(42, 200),
(99999, 404),
(0, 404),
(-1, 400),
])
def test_get_user_status_codes(api_session, user_id, expected_status):
response = api_session.get(f"{BASE_URL}/users/{user_id}")
assert response.status_code == expected_status
ينتج عن هذا أربع حالات اختبار منفصلة من دالة واحدة. كل منها يعمل ويبلغ بشكل مستقل، لذلك لا يخفي إدخال واحد سيء الحالات الأخرى. Parametrize هو إجابة pytest المدمجة للاختبار القائم على البيانات. عندما تنمو مجموعة المدخلات بشكل كبير، قم بتحميلها من ملف بدلاً من ذلك؛ يغطي دليلنا حول اختبار API القائم على البيانات باستخدام CSV و JSON هذا النمط. إذا لم تكن متأكدًا من رمز الحالة الذي يجب أن ترجعه كل إدخال، فإن المرجع حول رموز حالة HTTP التي يجب أن تستخدمها واجهات برمجة تطبيقات REST يعد رفيقًا مفيدًا.
التأكيد على جسم الاستجابة والمخطط
رموز الحالة ضرورية ولكنها ليست كافية. استجابة 200 مع جسم مشوه لا تزال خطأً. قم بالتأكيد على JSON المحلل مباشرة:
def test_order_response_shape(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
body = response.json()
assert isinstance(body["id"], int)
assert body["quantity"] == 2
assert body["total"] > 0
assert response.elapsed.total_seconds() < 1.0
لضمانات أقوى، قم بالتحقق من صحة الجسم مقابل مخطط JSON. هذا يكشف عن الانجراف الهيكلي، مثل حقل تمت إعادة تسميته أو مفقود، والتي تفوتها الفحوصات المكتوبة يدويًا:
from jsonschema import validate
order_schema = {
"type": "object",
"required": ["id", "product_id", "quantity", "status", "total"],
"properties": {
"id": {"type": "integer"},
"product_id": {"type": "integer"},
"quantity": {"type": "integer", "minimum": 1},
"status": {"type": "string"},
"total": {"type": "number"},
},
}
def test_order_matches_schema(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
validate(instance=response.json(), schema=order_schema)
يتوسع التحقق من صحة المخطط بشكل أفضل من تأكيدات الحقل تلو الآخر لأن مخططًا واحدًا يغطي شكل الاستجابة بالكامل. مكتبة jsonschema هي الخيار القياسي، وتشرح وثائق التحقق من صحتها الكلمات المفتاحية المدعومة.
تشغيل المجموعة في CI
تكسب مجموعة pytest قيمتها عندما تعمل تلقائيًا. يُرجع Pytest رمز خروج غير صفري عند الفشل، وهو بالضبط ما يحتاجه خادم CI لفشل البناء. قم بإصدار تقرير JUnit للعرض المباشر:
pytest -v --junitxml=results.xml
قم بتوصيل هذا الأمر بخطوة GitHub Actions أو أي خط أنابيب آخر، وستقوم اختبارات API الخاصة بك بحماية كل التزام. يوضح دليلنا الشامل لـ اختبارات API في خطوط أنابيب CI/CD الإعداد الكامل، بما في ذلك حقن الأسرار للرموز واختيار البيئة.
عادتان من عادات CI تحافظان على موثوقية مجموعة pytest. أولاً، لا تقم أبدًا بتشفير الأسرار أو عناوين URL للبيئة في ملفات الاختبار. اقرأها من متغيرات البيئة بحيث تعمل نفس المجموعة مقابل بيئة التطوير في CI ومحليًا بدون تعديلات:
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
ثانيًا، قم بتشغيل الاختبارات المستقلة بالتوازي للحفاظ على سرعة التغذية الراجعة. يقوم مكون pytest-xdist الإضافي بتوزيع الاختبارات عبر نوى وحدة المعالجة المركزية باستخدام pytest -n auto. تعمل عمليات التشغيل المتوازية فقط إذا كانت اختباراتك لا تشارك الحالة، وهذا سبب آخر لأهمية طبقة بيانات الاختبار. ستفشل المجموعة التي تعتمد على ترتيب التنفيذ بشكل غير متوقع لحظة تشغيلها بالتوازي.
الحفاظ على قابلية صيانة مجموعة pytest
مجموعة من خمسين اختبارًا سهلة. مجموعة من خمسمائة تحتاج إلى انضباط. ثلاث ممارسات تحافظ على صحة إطار عمل pytest API مع نموه.
قم بتجميع الاختبارات ذات الصلة في وحدات واستخدم الفئات فقط عندما تشارك في الإعداد، وليس للتزيين. ملف test_orders.py مع مجموعة واضحة من الدوال يُقرأ بشكل أفضل من ملف واحد ضخم. استخدم العلامات، المسجلة في pytest.ini، لتحديد الاختبارات بحيث يمكنك تشغيل مجموعات فرعية: @pytest.mark.smoke لبوابة سريعة، @pytest.mark.slow للمسح الكامل. قم بتشغيل مجموعة الاختبارات السريعة (smoke) على كل التزام ومجموعة الاختبارات الكاملة ليلاً.
قم بمركزة التكوين. عناوين URL الأساسية، والمخططات، والـ fixtures المشتركة تنتمي إلى conftest.py أو وحدة تكوين صغيرة، ولا يتم نسخها ولصقها أبدًا عبر الملفات. عندما يتغير عنوان URL الخاص ببيئة التطوير، يجب أن تقوم بتحرير سطر واحد. ينطبق نفس الانضباط المعياري الذي ينطبق على أي إطار عمل، والذي يغطيه دليلنا حول كتابة نصوص اختبار آلية، هنا: استخرج أي شيء تكتبه مرتين إلى fixture أو دالة مساعدة.
متى تلجأ إلى منصة بدلاً من ذلك
إطار عمل Pytest ممتاز عندما يكتب فريقك بلغة Python ويرغب في أن تكون الاختبارات موجودة بجانب كود التطبيق. إنه أقل ملاءمة عندما يحتاج موظفو ضمان الجودة أو المنتج إلى المساهمة، أو عندما ترغب في تصميم الاختبار، المحاكاة، والتنفيذ في مكان واحد دون الحاجة إلى صيانة كود ربط.
Apidog يغطي هذه الفجوة. فهو يوفر بناء اختبار مرئي، والتحقق من صحة المخطط مقابل مواصفات OpenAPI الخاصة بك، وتشغيلات تعتمد على البيانات من CSV و JSON، ومُشغل سطر أوامر لـ CI، كل ذلك دون الحاجة إلى كتابة كود الـ fixture والتأكيد يدويًا. تقوم العديد من الفرق بتشغيل كليهما: pytest للسيناريوهات المعقدة منطقياً و Apidog للتغطية الواسعة ولتصميم ومحاكاة واجهات برمجة التطبيقات التي يختبرها Pytest. يمكنك تنزيل Apidog ومقارنة النهجين على نقطة نهاية حقيقية في فترة بعد الظهر.
الأسئلة الشائعة
لماذا استخدام pytest بدلاً من unittest المدمج في Python لاختبار API؟
يحتاج Pytest إلى كود أقل تعقيدًا. الاختبارات هي دوال عادية، التأكيدات هي عبارات assert عادية مع إخراج فشل غني، والـ fixtures تتعامل مع الإعداد بمرونة أكبر من طرق unittest المعتمدة على الفئات. يحتوي Pytest أيضًا على نظام بيئي كبير من المكونات الإضافية و parametrize المدمج للاختبارات التي تعتمد على البيانات. لا يزال بإمكانه تشغيل الاختبارات الموجودة بأسلوب unittest، لذا فإن عملية الانتقال منخفضة المخاطر.
ما الفرق بين الـ fixture والـ parametrize؟
يوفر الـ fixture موردًا قابلاً لإعادة الاستخدام، مثل جلسة HTTP أو رمز مصادقة، لأي اختبار يطلبه. تقوم الـ parametrize بتشغيل جسم الاختبار نفسه عدة مرات مقابل قيم إدخال مختلفة. تشارك الـ fixtures الإعداد؛ تضاعف الـ parametrize الحالات. يمكن أن يتحدان جيدًا: لا يزال الاختبار المبرمج يمكن أن يعتمد على الـ fixtures.
هل يجب أن أؤكد على وقت الاستجابة في اختبارات API باستخدام pytest؟
يمكنك ذلك، باستخدام response.elapsed.total_seconds()، ويستطيع حد أعلى فضفاض أن يكشف عن التراجعات الكبيرة. ولكن pytest هو أداة اختبار وظيفية، وليس أداة لاختبار التحميل. لعمل أداء حقيقي، استخدم أداة مخصصة. حافظ على تأكيدات التوقيت سخية حتى لا يتسبب التباين الطبيعي للشبكة في حالات فشل متقطعة.
كيف أحافظ على استقلالية اختبارات API في pytest؟
امنح كل اختبار بياناته الخاصة من خلال الـ fixtures التي تنشئ وتزيل الموارد، وتجنب الاعتماد على ترتيب تنفيذ الاختبار. يقوم Pytest بتشغيل الاختبارات بترتيب الملفات افتراضيًا، ولكن المجموعة المصممة جيدًا لا تعتمد على ذلك. يمكن للاختبارات المستقلة أن تعمل بالتوازي وتفشل بشكل منفصل، مما يسهل عملية التصحيح بشكل كبير.
هل يمكن لـ pytest التحقق من صحة الاستجابات مقابل مواصفات OpenAPI؟
Pytest نفسه لا يفعل ذلك، ولكن يمكنك التحقق من صحة الاستجابات مقابل مخطط JSON باستخدام مكتبة jsonschema، وتوجد مكونات إضافية تتحقق من صحة الاستجابات مقابل مستند OpenAPI. إذا كان التحقق من صحة المخطط أمرًا أساسيًا لسير عملك، فقد توفر عليك منصة مثل Apidog التي تتحقق من صحة الاستجابات مقابل مواصفات OpenAPI الخاصة بك تلقائيًا إعداد المكون الإضافي.
