DEV Community

Cover image for إطار عمل Pytest للاختبار الآلي للواجهات البرمجية: دليل عملي
Yusuf Khalidd
Yusuf Khalidd

Posted on • Originally published at apidog.com

إطار عمل Pytest للاختبار الآلي للواجهات البرمجية: دليل عملي

يستخدم مطورو Python إطار pytest لاختبار واجهات API لأنه بسيط ومباشر: الاختبار دالة تبدأ بـ test_، والتأكيد عبارة assert عادية، وpytest يتولى الاكتشاف والتشغيل والتقارير. عند دمجه مع مكتبة requests تحصل على إطار عملي لاختبارات API يعتمد على الكود، بدون طبقات إضافية غير ضرورية.

جرّب Apidog اليوم

في هذا الدليل ستبني مجموعة اختبارات API قابلة للتوسيع باستخدام pytest. سنغطي إعداد المشروع، كتابة أول طلب، مشاركة الإعداد عبر fixtures، تشغيل نفس الاختبار على عدة مدخلات باستخدام parametrize، ثم التحقق من رمز الحالة، جسم الاستجابة، ومخطط JSON. كل الأمثلة قابلة للتعديل مباشرة على واجهاتك.

إعداد المشروع

ابدأ ببيئة افتراضية وثبّت الحزم المطلوبة:

python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
Enter fullscreen mode Exit fullscreen mode

استخدم هيكلًا بسيطًا يسهل توسيعه:

api-tests/
  conftest.py        # fixtures مشتركة
  test_users.py      # اختبارات نقاط نهاية المستخدمين
  test_orders.py     # اختبارات نقاط نهاية الطلبات
  pytest.ini         # إعدادات pytest
Enter fullscreen mode Exit fullscreen mode

يتعرف pytest على الاختبارات تلقائيًا إذا اتبعت القواعد التالية:

  • أسماء الملفات تبدأ بـ test_ أو تنتهي بـ _test.py
  • أسماء الدوال تبدأ بـ test_
  • أسماء فئات الاختبار تبدأ بـ Test
  • فئات الاختبار لا تحتوي على __init__

مثال إعداد أولي في pytest.ini:

[pytest]
testpaths = .
python_files = test_*.py *_test.py
python_functions = test_*
addopts = -v
markers =
    smoke: اختبارات سريعة للتأكد من سلامة النظام
    slow: اختبارات أبطأ أو أوسع نطاقًا
Enter fullscreen mode Exit fullscreen mode

إذا كانت الاختبارات الآلية جديدة عليك، فراجع دليلنا حول ما هي الاختبارات الآلية.

لماذا pytest لاختبار API؟ مكتبة requests تتعامل مع HTTP، بينما pytest يوفر الاكتشاف، التأكيدات المقروءة، fixtures، التشغيل القائم على البيانات عبر parametrize، والتقارير. بهذه التركيبة يمكنك بناء إطار اختبار API كامل من مكتبتين صغيرتين وموثقتين جيدًا.

كتابة أول اختبار API

اختبار API في pytest يتكون عادة من ثلاث خطوات:

  1. إرسال الطلب
  2. قراءة الاستجابة
  3. التحقق من القيم المتوقعة

مثال بسيط:

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
Enter fullscreen mode Exit fullscreen mode

أضف تحققًا من الحقول المهمة في جسم الاستجابة:

import requests

BASE_URL = "https://api.example.com/v1"

def test_get_user_returns_expected_fields():
    response = requests.get(f"{BASE_URL}/users/42")
    body = response.json()

    assert response.status_code == 200
    assert body["id"] == 42
    assert "email" in body
    assert body["status"] == "active"
Enter fullscreen mode Exit fullscreen mode

شغّل الاختبارات:

pytest -v
Enter fullscreen mode Exit fullscreen mode

عند فشل أي assert، يعرض pytest القيم الفعلية والمتوقعة بطريقة تساعدك على معرفة سبب الفشل بسرعة. لا تحتاج إلى دوال تأكيد خاصة؛ عبارات assert العادية كافية.

لأمثلة أكثر على ما يجب التحقق منه داخل الاستجابة، راجع دليل تأكيدات API.

مشاركة الإعداد باستخدام fixtures

لا تكرر عنوان API الأساسي أو إعداد جلسة HTTP في كل اختبار. استخدم fixtures.

الـ fixture هي دالة مزينة بـ @pytest.fixture وتُحقن في الاختبار عند طلبها كمعامل.

ضع الإعدادات المشتركة في 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()
Enter fullscreen mode Exit fullscreen mode

استخدمها داخل الاختبارات دون استيراد:

def test_get_user(api_session):
    response = api_session.get("https://api.example.com/v1/users/42")

    assert response.status_code == 200
Enter fullscreen mode Exit fullscreen mode

لتحسين الكود، اجعل BASE_URL أيضًا fixture:

# conftest.py
import os
import pytest
import requests

@pytest.fixture(scope="session")
def base_url():
    return os.environ.get("API_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()
Enter fullscreen mode Exit fullscreen mode

ثم استخدمه هكذا:

def test_get_user(api_session, base_url):
    response = api_session.get(f"{base_url}/users/42")

    assert response.status_code == 200
Enter fullscreen mode Exit fullscreen mode

إضافة fixture للمصادقة

إذا كانت نقاط النهاية تحتاج إلى token، أنشئ fixture مخصصًا:

# conftest.py
import pytest

@pytest.fixture
def auth_token(api_session, base_url):
    response = api_session.post(
        f"{base_url}/auth/login",
        json={
            "email": "qa@example.com",
            "password": "test-pass",
        },
    )

    assert response.status_code == 200
    return response.json()["token"]
Enter fullscreen mode Exit fullscreen mode

ثم استخدمه في اختبار إنشاء طلب:

def test_create_order(api_session, base_url, 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"
Enter fullscreen mode Exit fullscreen mode

scope="session" يعني أن fixture تُنشأ مرة واحدة لكل تشغيل. أما yield فيفصل الإعداد عن التنظيف: ما قبل yield يعمل قبل الاختبار، وما بعده يعمل عند انتهاء النطاق.

الـ fixtures هي البديل العملي للأساليب القديمة مثل setup_function وteardown_function. وهي موصى بها في وثائق pytest fixtures الرسمية.

تشغيل اختبار واحد مقابل عدة مدخلات

غالبًا تحتاج نقطة النهاية نفسها إلى اختبار عدة حالات:

  • قيمة صحيحة
  • قيمة غير موجودة
  • قيمة غير صالحة
  • حالة طرفية

بدل كتابة دالة لكل حالة، استخدم @pytest.mark.parametrize.

import pytest

@pytest.mark.parametrize(
    "user_id, expected_status",
    [
        (42, 200),
        (99999, 404),
        (0, 404),
        (-1, 400),
    ],
)
def test_get_user_status_codes(api_session, base_url, user_id, expected_status):
    response = api_session.get(f"{base_url}/users/{user_id}")

    assert response.status_code == expected_status
Enter fullscreen mode Exit fullscreen mode

سينتج pytest أربع حالات اختبار مستقلة من دالة واحدة. إذا فشلت حالة واحدة، ستظل الحالات الأخرى تعمل وتظهر في التقرير.

يمكنك أيضًا إضافة أسماء واضحة للحالات:

@pytest.mark.parametrize(
    "user_id, expected_status",
    [
        pytest.param(42, 200, id="existing-user"),
        pytest.param(99999, 404, id="missing-user"),
        pytest.param(0, 404, id="zero-id"),
        pytest.param(-1, 400, id="negative-id"),
    ],
)
def test_get_user_status_codes(api_session, base_url, user_id, expected_status):
    response = api_session.get(f"{base_url}/users/{user_id}")

    assert response.status_code == expected_status
Enter fullscreen mode Exit fullscreen mode

عندما تكبر مجموعة البيانات، حمّلها من ملف CSV أو JSON بدل كتابتها داخل الاختبار. يغطي دليلنا حول اختبار API القائم على البيانات باستخدام CSV و JSON هذا النمط.

وإذا لم تكن متأكدًا من رمز الحالة المناسب لكل سيناريو، راجع مرجع رموز حالة HTTP التي يجب أن تستخدمها واجهات REST API.

التأكيد على جسم الاستجابة والمخطط

رمز الحالة وحده لا يكفي. قد تحصل على 200 OK مع JSON ناقص أو غير متوافق.

ابدأ بتأكيدات مباشرة على الحقول المهمة:

def test_order_response_shape(api_session, base_url, 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 response.status_code == 201
    assert isinstance(body["id"], int)
    assert body["product_id"] == 7
    assert body["quantity"] == 2
    assert body["total"] > 0
    assert response.elapsed.total_seconds() < 1.0
Enter fullscreen mode Exit fullscreen mode

استخدم حدًا زمنيًا واسعًا لتجنب فشل متقطع بسبب الشبكة. اختبارات pytest هنا وظيفية وليست بديلًا لاختبارات الأداء أو التحميل.

التحقق من JSON Schema

لضمان أقوى، تحقق من جسم الاستجابة مقابل مخطط 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, base_url, 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
    validate(instance=response.json(), schema=order_schema)
Enter fullscreen mode Exit fullscreen mode

التحقق من المخطط يساعدك على اكتشاف تغييرات مثل:

  • حذف حقل مطلوب
  • تغيير نوع حقل
  • إعادة تسمية خاصية
  • السماح بقيم غير متوقعة

مكتبة jsonschema هي الخيار القياسي، وتشرح وثائق التحقق من صحتها الكلمات المفتاحية المدعومة.

تشغيل المجموعة في CI

تصبح اختبارات API أكثر قيمة عندما تعمل تلقائيًا عند كل تغيير. pytest يعيد رمز خروج غير صفري عند الفشل، وهذا ما تحتاجه أدوات CI لإيقاف البناء.

شغّل الاختبارات مع تقرير JUnit:

pytest -v --junitxml=results.xml
Enter fullscreen mode Exit fullscreen mode

مثال خطوة بسيطة في GitHub Actions:

name: API Tests

on:
  push:
  pull_request:

jobs:
  api-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pytest requests jsonschema

      - name: Run API tests
        env:
          API_BASE_URL: ${{ secrets.API_BASE_URL }}
        run: |
          pytest -v --junitxml=results.xml
Enter fullscreen mode Exit fullscreen mode

يوضح دليلنا الشامل حول اختبارات API في خطوط أنابيب CI/CD إعدادًا كاملًا يشمل الأسرار واختيار البيئة.

لا تضع الأسرار داخل الاختبارات

اقرأ القيم من متغيرات البيئة:

import os

BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
Enter fullscreen mode Exit fullscreen mode

طبق نفس النمط على:

  • رموز الوصول
  • بيانات تسجيل الدخول
  • عناوين البيئات
  • مفاتيح API

شغّل الاختبارات بالتوازي عند الحاجة

للمجموعات الكبيرة، استخدم pytest-xdist:

pip install pytest-xdist
pytest -n auto
Enter fullscreen mode Exit fullscreen mode

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

الحفاظ على قابلية صيانة مجموعة pytest

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

1. قسّم الاختبارات حسب المجال

استخدم ملفات واضحة:

test_users.py
test_orders.py
test_payments.py
test_auth.py
Enter fullscreen mode Exit fullscreen mode

تجنب ملفًا واحدًا ضخمًا يحتوي على كل شيء.

2. استخدم العلامات markers

مثال اختبار smoke:

import pytest

@pytest.mark.smoke
def test_health_check(api_session, base_url):
    response = api_session.get(f"{base_url}/health")

    assert response.status_code == 200
Enter fullscreen mode Exit fullscreen mode

شغّل اختبارات smoke فقط:

pytest -m smoke
Enter fullscreen mode Exit fullscreen mode

شغّل كل شيء باستثناء الاختبارات البطيئة:

pytest -m "not slow"
Enter fullscreen mode Exit fullscreen mode

سجّل العلامات في pytest.ini:

[pytest]
markers =
    smoke: اختبارات سريعة
    slow: اختبارات بطيئة أو واسعة
Enter fullscreen mode Exit fullscreen mode

3. ركّز التكوين في مكان واحد

لا تنسخ القيم في كل ملف. ضع العناصر المشتركة في:

  • conftest.py
  • وحدة إعدادات صغيرة
  • fixtures
  • دوال مساعدة

إذا كررت شيئًا مرتين، فغالبًا يجب تحويله إلى fixture أو helper.

ينطبق نفس الانضباط المعياري الذي يغطيه دليل كتابة نصوص اختبار آلية: اجعل الاختبار يصف السلوك، وانقل التفاصيل المتكررة إلى طبقة مشتركة.

متى تستخدم منصة بدلًا من الكود فقط؟

pytest ممتاز عندما:

  • فريقك مرتاح مع Python
  • تريد وضع الاختبارات بجانب كود التطبيق
  • تحتاج إلى منطق اختبار مخصص
  • تريد تحكمًا كاملًا في التنفيذ

لكنه قد يكون أقل ملاءمة عندما:

  • يحتاج فريق QA أو المنتج إلى المساهمة دون كتابة Python
  • تريد تصميم API والمحاكاة والاختبار في مكان واحد
  • لا تريد صيانة fixtures وتأكيدات مخصصة لكل سيناريو

Apidog يغطي هذه الفجوة. يوفر بناء اختبارات مرئيًا، والتحقق من صحة المخطط مقابل مواصفات OpenAPI، وتشغيلات تعتمد على CSV وJSON، ومُشغل سطر أوامر لـ CI، دون كتابة كود fixtures والتأكيدات يدويًا.

تستخدم بعض الفرق النهجين معًا: pytest للسيناريوهات ذات المنطق المعقد، وApidog للتغطية الواسعة وتصميم ومحاكاة واجهات API التي تختبرها pytest. يمكنك تنزيل Apidog وتجربة النهجين على نقطة نهاية حقيقية.

الأسئلة الشائعة

لماذا أستخدم pytest بدلًا من unittest المدمج في Python لاختبار API؟

pytest يحتاج إلى كود أقل. الاختبارات دوال عادية، والتأكيدات عبارات assert مباشرة مع رسائل فشل غنية، والـ fixtures أكثر مرونة من إعدادات unittest المعتمدة على الفئات. كما يوفر pytest نظام إضافات واسعًا وparametrize مدمجًا للاختبارات القائمة على البيانات.

ما الفرق بين fixture و parametrize؟

الـ fixture توفر موردًا قابلًا لإعادة الاستخدام، مثل جلسة HTTP أو token مصادقة. أما parametrize فتشغّل نفس جسم الاختبار عدة مرات بقيم مختلفة.

يمكن استخدامهما معًا:

@pytest.mark.parametrize("user_id", [1, 2, 3])
def test_get_user(api_session, base_url, user_id):
    response = api_session.get(f"{base_url}/users/{user_id}")

    assert response.status_code == 200
Enter fullscreen mode Exit fullscreen mode

هل يجب تأكيد وقت الاستجابة في اختبارات API باستخدام pytest؟

يمكنك ذلك عبر:

assert response.elapsed.total_seconds() < 1.0
Enter fullscreen mode Exit fullscreen mode

لكن اجعل الحد واسعًا لتجنب فشل متقطع بسبب الشبكة. pytest مناسب للاختبارات الوظيفية، وليس بديلًا لاختبارات الأداء أو التحميل.

كيف أحافظ على استقلالية اختبارات API في pytest؟

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

هل يمكن لـ pytest التحقق من صحة الاستجابات مقابل مواصفات OpenAPI؟

pytest لا يفعل ذلك مباشرة، لكن يمكنك استخدام jsonschema للتحقق من مخططات JSON، أو استخدام إضافات تتحقق من الاستجابات مقابل مستند OpenAPI. إذا كان التحقق من صحة المخطط جزءًا أساسيًا من سير عملك، فقد توفر منصة مثل Apidog إعدادًا جاهزًا للتحقق مقابل مواصفات OpenAPI تلقائيًا.

Top comments (0)