404 Not Found

404 Not Found


nginx

الاختبار — ضمان الجودة الشامل مع pytest

الاختبار مثل شبكة الأمان—لا يمكنك رؤيتها أثناء المشي على الحبل (كتابة الكود)، لكن إذا انزلقت (واجهت خطأ)، فهي خط حياتك الوحيد. الأداء بدون شبكة أمان محكوم عليه بالحادث عاجلًا أم آجلًا.

1. ما ستتعلمه


2. القصة الحقيقية لـ Alice

(1) نقطة الألم: اكتشاف الأخطاء فقط بعد الإطلاق

بعد نشر PriceTracker، اكتشفت Alice أن وقت انتهاء صلاحية رمز JWT كان معينًا إلى ثانية واحدة، وأن الاستيراد المجمّع كان يتخطى جميع التحققات، وأن تحديد المعدل للمستخدمين Pro كان نفسه للمستخدمين المجانيين. في كل مرة تصلح خطأً، تُدخل خطأً جديدًا، واشتكى Bob: الميزة التي كانت تعمل الأسبوع الماضي لا تعمل هذا الأسبوع مرة أخرى. لم يكن لدى Alice اختبارات آلية؛ اعتمدت بالكامل على النقر يدويًا عبر Swagger UI، وكل اختبار انحدار استغرق ساعتين.

(2) حلول اختبار pytest الآلي

pytest + httpx TestClient يضمن أن كل نقطة نهاية API لديها اختبارات آلية؛ dependency_overrides يستبدل قاعدة بيانات الإنتاج بقاعدة بيانات اختبار؛ وفي كل مرة يتم تشغيل git push، تُنفذ جميع الاختبارات تلقائيًا، مما يكتشف جميع مشاكل الانحدار في دقيقتين.

PYTHON
def test_create_product(client):
    response = client.post("/api/v1/products", json={"name": "Widget", "price": 9.99})
    assert response.status_code == 201
    assert response.json()["name"] == "Widget"

(3) العائد

انتقل اختبار الانحدار من عملية يدوية مدتها ساعتان إلى عملية آلية مدتها دقيقتان؛ تم اكتشاف خطأ انتهاء صلاحية JWT خلال مرحلة الاختبار في التطوير؛ وبعد النشر، انخفض عدد الأخطاء من 5 أسبوعيًا إلى 1 شهريًا.


3. أساسيات TestClient

(1) هرم الاختبار

100%
graph TD
    E2E[اختبارات E2E - قليلة] --> INT[اختبارات التكامل - متوسطة]
    INT --> UNIT[اختبارات الوحدة - كثيرة]
    
    UNIT --- U1[التحقق من نموذج Pydantic]
    UNIT --- U2[دوال Repository]
    UNIT --- U3[منطق الخدمة]
    
    INT --- I1[نقطة نهاية API + DB]
    INT --- I2[تدفق المصادقة]
    INT --- I3[عمليات CRUD]
    
    E2E --- E1[رحلة المستخدم الكاملة]
    E2E --- E2[WebSocket + API]
المستوى الكمية السرعة الاعتمادات
اختبارات الوحدة كثيرة (100+) سريعة (< 1 مللي ثانية) بدون اعتمادات خارجية
اختبار التكامل متوسطة (30-50) متوسطة (10-100 مللي ثانية) قاعدة بيانات اختبار
اختبار E2E قليلة (5-10) بطيئة (1-5 ثوانٍ) بيئة كاملة

(1) ▶ مثال: Fixture أساسي لـ TestClient

PYTHON
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.core.deps import get_db

# تجاوز اعتماد قاعدة البيانات
def get_test_db():
    # استخدام SQLite في الذاكرة للاختبار
    engine = create_async_engine("sqlite+aiosqlite:///test.db")
    # ... إعداد الجلسة
    yield session
    # ... تنظيف

@pytest.fixture
def client():
    app.dependency_overrides[get_db] = get_test_db
    with TestClient(app) as c:
        yield c
    app.dependency_overrides.clear()

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) ▶ مثال: اختبار نقطة نهاية أساسية

PYTHON
# tests/test_health.py
def test_health_check(client):
    response = client.get("/health")
    assert response.status_code == 200
    data = response.json()
    assert data["status"] == "healthy"
    assert "service" in data

def test_openapi_docs_available(client):
    response = client.get("/docs")
    assert response.status_code == 200

الناتج:

TEXT
# تم تعريف الدالة بنجاح

4. عزل قاعدة بيانات الاختبار

(1) قاعدة بيانات منفصلة لكل اختبار

(1) ▶ مثال: Fixture قاعدة بيانات الاختبار

PYTHON
# tests/conftest.py
import pytest
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.models import Base

TEST_DATABASE_URL = "sqlite+aiosqlite:///test_pricetracker.db"

@pytest.fixture(scope="function")
async def test_db():
    # إنشاء قاعدة بيانات اختبار جديدة لكل اختبار
    engine = create_async_engine(TEST_DATABASE_URL, echo=False)
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    
    TestSession = async_sessionmaker(engine, expire_on_commit=False)
    async with TestSession() as session:
        yield session
    
    # حذف جميع الجداول بعد الاختبار
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
    await engine.dispose()

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) ▶ مثال: عميل اختبار غير متزامن

PYTHON
# tests/conftest.py
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app

@pytest.fixture
async def async_client():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) مقارنة استراتيجيات العزل

الاستراتيجية السرعة العزل قابلية التطبيق
إنشاء جداول جديدة لكل اختبار بطيئة معزول بالكامل اختبار سلامة البيانات
إرجاع المعاملة سريعة جيد معظم اختبارات التكامل
SQLite في الذاكرة سريعة جيد اختبارات لا تعتمد على ميزات PostgreSQL

5. تقنيات تجاوز الاعتماد

(1) استبدال المصادقة وقاعدة البيانات

(1) ▶ مثال: تجاوز اعتمادات المصادقة

PYTHON
# tests/conftest.py
from app.core.deps import get_current_user, get_db

def get_test_user():
    """مستخدم مصادق محاكي للاختبار"""
    return {"id": 1, "email": "alice@test.com", "role": "admin", "subscription": "pro"}

@pytest.fixture
def auth_client(client):
    # تجاوز المصادقة - لا حاجة لـ JWT حقيقي
    app.dependency_overrides[get_current_user] = get_test_user
    yield client
    app.dependency_overrides.clear()

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) ▶ مثال: اختبار نقاط النهاية المحمية

PYTHON
# tests/test_products.py
def test_list_products_unauthorized(client):
    """بدون رمز مصادقة، يجب أن يعيد 401"""
    response = client.get("/api/v1/products")
    assert response.status_code == 401

def test_list_products_authorized(auth_client):
    """مع مصادقة محاكية، يجب أن يعيد 200"""
    response = auth_client.get("/api/v1/products")
    assert response.status_code == 200

def test_create_product(auth_client):
    response = auth_client.post(
        "/api/v1/products",
        json={"name": "Widget", "category": "electronics", "base_price": 29.99},
    )
    assert response.status_code == 201
    assert response.json()["name"] == "Widget"

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(3) ▶ مثال: تجاوز اختبار صلاحيات مستوى الاشتراك

PYTHON
def get_free_user():
    return {"id": 2, "email": "free@test.com", "role": "user", "subscription": "free"}

def get_pro_user():
    return {"id": 3, "email": "pro@test.com", "role": "user", "subscription": "pro"}

def test_bulk_import_free_user_limited(client):
    """المستخدمون المجانيون يمكنهم استيراد 1000 سعر كحد أقصى"""
    app.dependency_overrides[get_current_user] = get_free_user
    prices = [{"product_id": i, "price": 9.99} for i in range(1500)]
    response = client.post("/api/v1/prices/bulk", json=prices)
    assert response.status_code == 403
    assert "limit" in response.json()["detail"].lower()
    app.dependency_overrides.clear()

def test_bulk_import_pro_user(client):
    """المستخدمون Pro يمكنهم استيراد ما يصل إلى 100000 سعر"""
    app.dependency_overrides[get_current_user] = get_pro_user
    prices = [{"product_id": i, "price": 9.99} for i in range(5000)]
    response = client.post("/api/v1/prices/bulk", json=prices)
    assert response.status_code == 201
    app.dependency_overrides.clear()

الناتج:

TEXT
# تم تعريف الدالة بنجاح

6. مثال على مجموعة اختبارات كاملة

(1) ▶ مثال: اختبار وحدة لنماذج Pydantic

PYTHON
# tests/test_models.py
import pytest
from pydantic import ValidationError
from app.schemas import ProductCreate, PriceCreate

def test_product_create_valid():
    p = ProductCreate(name="Widget", category="electronics", base_price=29.99)
    assert p.name == "Widget"
    assert p.base_price == 29.99

def test_product_create_negative_price():
    with pytest.raises(ValidationError) as exc:
        ProductCreate(name="Widget", category="electronics", base_price=-1)
    assert "greater than 0" in str(exc.value)

def test_product_create_empty_name():
    with pytest.raises(ValidationError):
        ProductCreate(name="", category="electronics", base_price=9.99)

def test_price_create_rounds_precision():
    p = PriceCreate(product_id=1, price=9.999, currency="USD", source="test")
    assert p.price == 10.0  # تقريب إلى منزلتين عشريتين

def test_price_create_invalid_currency():
    with pytest.raises(ValidationError):
        PriceCreate(product_id=1, price=9.99, currency="XYZ", source="test")

الناتج:

TEXT
# تم تعريف الدالة بنجاح

(2) ▶ مثال: اختبار تكامل لعمليات CRUD

PYTHON
# tests/test_crud.py
import pytest
from fastapi.testclient import TestClient

def test_product_crud_lifecycle(auth_client):
    # إنشاء
    create_resp = auth_client.post(
        "/api/v1/products",
        json={"name": "Test Widget", "category": "electronics", "base_price": 19.99},
    )
    assert create_resp.status_code == 201
    product_id = create_resp.json()["id"]

    # قراءة
    get_resp = auth_client.get(f"/api/v1/products/{product_id}")
    assert get_resp.status_code == 200
    assert get_resp.json()["name"] == "Test Widget"

    # تحديث
    update_resp = auth_client.put(
        f"/api/v1/products/{product_id}",
        json={"name": "Updated Widget", "base_price": 24.99},
    )
    assert update_resp.status_code == 200
    assert update_resp.json()["base_price"] == 24.99

    # حذف
    delete_resp = auth_client.delete(f"/api/v1/products/{product_id}")
    assert delete_resp.status_code == 200

    # التحقق من الحذف
    get_resp2 = auth_client.get(f"/api/v1/products/{product_id}")
    assert get_resp2.status_code == 404

الناتج:

TEXT
# تم تعريف الدالة بنجاح

❓ أسئلة شائعة

س أيهما أفضل للاستخدام، TestClient أم AsyncClient؟
ج استخدم TestClient (متزامن) للاختبارات البسيطة، واستخدم AsyncClient عندما تحتاج لاختبار منطق غير متزامن (مثل WebSocket). TestClient كافٍ لمعظم السيناريوهات.
س هل يجب استخدام SQLite أم PostgreSQL لقاعدة بيانات الاختبار؟
ج استخدم SQLite للتطوير (أسرع) و PostgreSQL لـ CI (لمطابقة بيئة الإنتاج). لاحظ أن SQLite لا يدعم بعض ميزات PostgreSQL (مثل RETURNING و JSONB).
س كيف يجب اختيار scope لـ fixtures؟
ج function (تُعاد إنشاؤها لكل اختبار) هي الأكثر أمانًا؛ session (مشتركة عبر جلسة الاختبار بأكملها) هي الأسرع لكن العزل ضعيف. استخدم function لقاعدة البيانات و function لـ TestClient.
س كيف أختبر WebSocket؟
ج استخدم AsyncClient.websocket_connect() لإنشاء اتصال، وأرسل واستقبل رسائل للتحقق من سلوكه.
س ماذا يجب أن أفعل إذا كان الاختبار يعمل ببطء شديد؟
ج استخدم pytest-xdist للتنفيذ المتوازي (pytest -n auto)، واستخدم إرجاع المعاملة بدلاً من إنشاء الجداول، واستخدم اختبارات الوحدة بدلاً من اختبارات التكامل.
س هل سيؤثر dependency_overrides على اختبارات أخرى؟
ج نعم، لأنه يعدل الكائن العام للتطبيق. تأكد من وضع app.dependency_overrides.clear() بعد عبارة yield في fixture.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة ⭐): استخدم TestClient لكتابة اختبار لنقطة النهاية /health للتحقق من أنها تعيد كود حالة 200 وبنية JSON الصحيحة. تلميح: TestClient(app) + client.get("/health")
  2. تمرين متقدم (الصعوبة ⭐⭐): اكتب اختبارات دورة حياة CRUD—إنشاء واستعلام وتحديث وحذف المنتجات—وتحقق من رموز الحالة والبيانات المرجعة لكل خطوة. استخدم dependency_overrides لتخطي المصادقة. تلميح: app.dependency_overrides[get_current_user] = mock_fn
  3. تحدي (الصعوبة ⭐⭐⭐): نفذ مجموعة اختبارات كاملة: اختبارات التحقق من Pydantic (البيانات غير الصالحة تثير ValidationError)، اختبارات المصادقة (401 بدون رمز، 403 للمستخدمين المجانيين، 200 للمستخدمين Pro)، واختبارات التقسيم (إرجاع صحيح لقيم skip و limit). تلميح: pytest.raises(ValidationError) + fixtures متعددة dependency_overrides

---|

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%