تعلم كيفية اختبار تطبيقات FastAPI الخاصة بك بفعالية باستخدام TestClient. تغطي أفضل الممارسات والتقنيات المتقدمة وأمثلة واقعية لواجهات برمجة تطبيقات قوية وموثوقة.
إتقان اختبار FastAPI: دليل شامل لـ TestClient
برزت FastAPI كإطار عمل رائد لبناء واجهات برمجة تطبيقات عالية الأداء باستخدام بايثون. إن سرعتها وسهولة استخدامها والتحقق التلقائي من البيانات تجعلها المفضلة بين المطورين في جميع أنحاء العالم. ومع ذلك، فإن واجهة برمجة تطبيقات جيدة البناء لا تقل أهمية عن اختباراتها. يضمن الاختبار الشامل أن واجهة برمجة التطبيقات الخاصة بك تعمل كما هو متوقع، وتبقى مستقرة تحت الضغط، ويمكن نشرها بثقة في الإنتاج. يركز هذا الدليل الشامل على استخدام TestClient الخاص بـ FastAPI لاختبار نقاط نهاية API الخاصة بك بشكل فعال.
لماذا يعتبر الاختبار مهمًا لتطبيقات FastAPI؟
الاختبار هو خطوة حاسمة في دورة حياة تطوير البرمجيات. فهو يساعدك على:
- تحديد الأخطاء مبكرًا: اكتشاف الأخطاء قبل وصولها إلى الإنتاج، مما يوفر الوقت والموارد.
- ضمان جودة التعليمات البرمجية: تعزيز التعليمات البرمجية جيدة التنظيم والقابلة للصيانة.
- منع الانحدار: ضمان عدم كسر التغييرات الجديدة للوظائف الحالية.
- تحسين موثوقية واجهة برمجة التطبيقات: بناء الثقة في استقرار وأداء واجهة برمجة التطبيقات.
- تسهيل التعاون: توفير وثائق واضحة للسلوك المتوقع للمطورين الآخرين.
تقديم TestClient الخاص بـ FastAPI
توفر FastAPI TestClient مضمنًا يبسط عملية اختبار نقاط نهاية API الخاصة بك. يعمل TestClient كعميل خفيف الوزن يمكنه إرسال طلبات إلى واجهة برمجة التطبيقات الخاصة بك دون بدء تشغيل خادم كامل. وهذا يجعل الاختبار أسرع وأكثر ملاءمة بشكل كبير.
الميزات الرئيسية لـ TestClient:
- محاكاة طلبات HTTP: يسمح لك بإرسال طلبات GET وPOST وPUT وDELETE وطلبات HTTP الأخرى إلى واجهة برمجة التطبيقات الخاصة بك.
- معالجة تسلسل البيانات: يقوم تلقائيًا بتسلسل بيانات الطلب (على سبيل المثال، حمولات JSON) وإلغاء تسلسل بيانات الاستجابة.
- يوفر طرق التأكيد: يقدم طرقًا ملائمة للتحقق من رمز الحالة والرؤوس ومحتوى الاستجابات.
- يدعم الاختبار غير المتزامن: يعمل بسلاسة مع الطبيعة غير المتزامنة لـ FastAPI.
- يتكامل مع أطر الاختبار: يتكامل بسهولة مع أطر اختبار بايثون الشائعة مثل pytest و unittest.
إعداد بيئة الاختبار الخاصة بك
قبل البدء في الاختبار، تحتاج إلى إعداد بيئة الاختبار الخاصة بك. يتضمن هذا عادةً تثبيت التبعيات الضرورية وتكوين إطار الاختبار الخاص بك.
التثبيت
أولاً، تأكد من تثبيت FastAPI و pytest. يمكنك تثبيتها باستخدام pip:
pip install fastapi pytest httpx
httpx هو عميل HTTP الذي تستخدمه FastAPI تحت الغطاء. في حين أن TestClient هو جزء من FastAPI، فإن وجود httpx مثبتًا أيضًا يضمن اختبارًا سلسًا. تذكر بعض البرامج التعليمية أيضًا requests، ومع ذلك، فإن httpx يتماشى بشكل أكبر مع الطبيعة غير المتزامنة لـ FastAPI.
مثال على تطبيق FastAPI
لنقم بإنشاء تطبيق FastAPI بسيط يمكننا استخدامه للاختبار:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items/")
async def create_item(item: Item):
return item
احفظ هذا الرمز باسم main.py. يحدد هذا التطبيق ثلاث نقاط نهاية:
/: نقطة نهاية GET بسيطة ترجع رسالة "Hello World"./items/{item_id}: نقطة نهاية GET ترجع عنصرًا بناءً على معرفه./items/: نقطة نهاية POST تقوم بإنشاء عنصر جديد.
كتابة الاختبار الأول
الآن بعد أن أصبح لديك تطبيق FastAPI، يمكنك البدء في كتابة الاختبارات باستخدام TestClient. قم بإنشاء ملف جديد باسم test_main.py في نفس الدليل مثل main.py.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
في هذا الاختبار:
- نستورد
TestClientومثيلappالخاص بـ FastAPI. - نقوم بإنشاء مثيل لـ
TestClient، وتمريرapp. - نقوم بتحديد دالة اختبار
test_read_root. - داخل دالة الاختبار، نستخدم
client.get("/")لإرسال طلب GET إلى نقطة النهاية الجذرية. - نؤكد أن رمز حالة الاستجابة هو 200 (موافق).
- نؤكد أن JSON للاستجابة يساوي
{"message": "Hello World"}.
تشغيل اختباراتك باستخدام pytest
لتشغيل اختباراتك، ما عليك سوى فتح Terminal في الدليل الذي يحتوي على ملف test_main.py الخاص بك وتشغيل الأمر التالي:
pytest
سيكتشف pytest تلقائيًا جميع الاختبارات في مشروعك ويقوم بتشغيلها. يجب أن ترى إخراجًا مشابهًا لما يلي:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 1 item
test_main.py .
============================== 1 passed in 0.01s ==============================
اختبار طرق HTTP المختلفة
يدعم TestClient جميع طرق HTTP القياسية، بما في ذلك GET وPOST وPUT وDELETE وPATCH. لنرى كيفية اختبار كل من هذه الطرق.
اختبار طلبات GET
لقد رأينا بالفعل مثالاً على اختبار طلب GET في القسم السابق. إليك مثال آخر، وهو اختبار نقطة النهاية /items/{item_id}:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
يرسل هذا الاختبار طلب GET إلى /items/1 مع معلمة الاستعلام q=test. ثم يؤكد أن رمز حالة الاستجابة هو 200 وأن JSON للاستجابة يحتوي على البيانات المتوقعة.
اختبار طلبات POST
لاختبار طلب POST، تحتاج إلى إرسال بيانات في نص الطلب. يقوم TestClient تلقائيًا بتسلسل البيانات إلى JSON.
def test_create_item():
item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}
response = client.post("/items/", json=item_data)
assert response.status_code == 200
assert response.json() == item_data
في هذا الاختبار:
- نقوم بإنشاء قاموس
item_dataيحتوي على بيانات العنصر الجديد. - نستخدم
client.post("/items/", json=item_data)لإرسال طلب POST إلى نقطة النهاية/items/، وتمريرitem_dataكحمولة JSON. - نؤكد أن رمز حالة الاستجابة هو 200 وأن JSON للاستجابة يطابق
item_data.
اختبار طلبات PUT وDELETE وPATCH
يشبه اختبار طلبات PUT وDELETE وPATCH اختبار طلبات POST. ما عليك سوى استخدام الطرق المقابلة في TestClient:
def test_update_item():
item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}
response = client.put("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Add assertions for the expected response
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Add assertions for the expected response
تذكر إضافة تأكيدات للتحقق من أن الاستجابات كما هو متوقع.
تقنيات الاختبار المتقدمة
يقدم TestClient العديد من الميزات المتقدمة التي يمكن أن تساعدك في كتابة اختبارات أكثر شمولاً وفعالية.
الاختبار مع التبعيات
يسمح لك نظام حقن التبعية في FastAPI بحقن التبعيات بسهولة في نقاط نهاية API الخاصة بك. عند الاختبار، قد ترغب في تجاوز هذه التبعيات لتوفير عمليات تنفيذ وهمية أو خاصة بالاختبار.
على سبيل المثال، افترض أن تطبيقك يعتمد على اتصال قاعدة بيانات. يمكنك تجاوز تبعية قاعدة البيانات في اختباراتك لاستخدام قاعدة بيانات في الذاكرة:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session
# Database Configuration
DATABASE_URL = "sqlite:///./test.db" # In-memory database for testing
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Define User Model
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String)
Base.metadata.create_all(bind=engine)
# FastAPI App
app = FastAPI()
# Dependency to get the database session
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Endpoint to create a user
@app.post("/users/")
async def create_user(username: str, password: str, db: Session = Depends(get_db)):
db_user = User(username=username, password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi.testclient import TestClient
from .main import app, get_db, Base, engine, TestingSessionLocal
client = TestClient(app)
# Override the database dependency for testing
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# First, ensure the tables are created, which may not happen by default
Base.metadata.create_all(bind=engine) # important: create the tables in the test db
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Clean up the override after the test if needed
app.dependency_overrides = {}
يستبدل هذا المثال التبعية get_db بدالة خاصة بالاختبار تُرجع جلسة إلى قاعدة بيانات SQLite في الذاكرة. هام: يجب استدعاء إنشاء البيانات التعريفية بشكل صريح لكي تعمل قاعدة بيانات الاختبار بشكل صحيح. سيؤدي الفشل في إنشاء الجدول إلى أخطاء تتعلق بالجداول المفقودة.
اختبار التعليمات البرمجية غير المتزامنة
تم تصميم FastAPI ليكون غير متزامن، لذلك ستحتاج غالبًا إلى اختبار التعليمات البرمجية غير المتزامنة. يدعم TestClient الاختبار غير المتزامن بسلاسة.
لاختبار نقطة نهاية غير متزامنة، ما عليك سوى تحديد دالة الاختبار الخاصة بك على أنها async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Simulate some async operation
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Needed to be compatible with pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
ملاحظة: تحتاج إلى تثبيت pytest-asyncio لاستخدام @pytest.mark.asyncio: pip install pytest-asyncio. تحتاج أيضًا إلى التأكد من تكوين asyncio.get_event_loop() إذا كنت تستخدم إصدارات pytest الأقدم. إذا كنت تستخدم الإصدار 8 أو أحدث من pytest، فقد لا يكون ذلك مطلوبًا.
اختبار تحميل الملفات
FastAPI يجعل من السهل التعامل مع تحميل الملفات. لاختبار تحميل الملفات، يمكنك استخدام المعلمة files لطرق طلب TestClient.
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
@app.post("/files/")
async def create_files(files: List[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}
from fastapi.testclient import TestClient
from .main import app
import io
client = TestClient(app)
def test_create_files():
file_content = b"Test file content"
files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/files/", files=files)
assert response.status_code == 200
assert response.json() == {"file_sizes": [len(file_content)]}
def test_create_upload_files():
file_content = b"Test upload file content"
files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/uploadfiles/", files=files)
assert response.status_code == 200
assert response.json() == {"filenames": ["test_upload.txt"]}
في هذا الاختبار، نقوم بإنشاء ملف وهمي باستخدام io.BytesIO وتمريره إلى المعلمة files. تقبل المعلمة files قائمة من الصفوف، حيث يحتوي كل صف على اسم الحقل واسم الملف ومحتوى الملف. يعد نوع المحتوى مهمًا للمعالجة الدقيقة بواسطة الخادم.
اختبار معالجة الأخطاء
من المهم اختبار كيفية تعامل واجهة برمجة التطبيقات الخاصة بك مع الأخطاء. يمكنك استخدام TestClient لإرسال طلبات غير صالحة والتحقق من أن واجهة برمجة التطبيقات ترجع استجابات الخطأ الصحيحة.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=400, detail="Item ID too large")
return {"item_id": item_id}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item_error():
response = client.get("/items/101")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID too large"}
يرسل هذا الاختبار طلب GET إلى /items/101، والذي يثير HTTPException برمز حالة 400. يؤكد الاختبار أن رمز حالة الاستجابة هو 400 وأن JSON للاستجابة يحتوي على رسالة الخطأ المتوقعة.
اختبار ميزات الأمان
إذا كانت واجهة برمجة التطبيقات الخاصة بك تستخدم المصادقة أو التخويل، فستحتاج إلى اختبار ميزات الأمان هذه أيضًا. يتيح لك TestClient تعيين الرؤوس وملفات تعريف الارتباط لمحاكاة الطلبات المصادق عليها.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Security
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simulate authentication
if form_data.username != "testuser" or form_data.password != "password123":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
return {"access_token": "fake_token", "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
return {"message": "Protected data"}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_login():
response = client.post("/token", data={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route():
# First, get a token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Then, use the token to access the protected route
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # corrected format.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
في هذا المثال، نختبر نقطة نهاية تسجيل الدخول ثم نستخدم полученный token للوصول إلى مسار محمي. تتيح لك المعلمة headers لطرق طلب TestClient تعيين رؤوس مخصصة، بما في ذلك رأس Authorization لرموز حاملة.
أفضل الممارسات لاختبار FastAPI
فيما يلي بعض أفضل الممارسات التي يجب اتباعها عند اختبار تطبيقات FastAPI الخاصة بك:
- اكتب اختبارات شاملة: استهدف تغطية اختبار عالية لضمان اختبار جميع أجزاء واجهة برمجة التطبيقات الخاصة بك بدقة.
- استخدم أسماء اختبار وصفية: تأكد من أن أسماء الاختبار الخاصة بك تشير بوضوح إلى ما يتحقق منه الاختبار.
- اتبع نمط الترتيب والعمل والتأكيد: قم بتنظيم اختباراتك في ثلاث مراحل متميزة: الترتيب (إعداد بيانات الاختبار)، والعمل (تنفيذ الإجراء الذي يتم اختباره)، والتأكيد (التحقق من النتائج).
- استخدم كائنات وهمية: قم بمحاكاة التبعيات الخارجية لعزل اختباراتك وتجنب الاعتماد على الأنظمة الخارجية.
- اختبر الحالات المتطرفة: اختبر واجهة برمجة التطبيقات الخاصة بك بإدخال غير صالح أو غير متوقع للتأكد من أنها تتعامل مع الأخطاء بأمان.
- تشغيل الاختبارات بشكل متكرر: قم بدمج الاختبار في سير عمل التطوير الخاص بك لاكتشاف الأخطاء مبكرًا وفي كثير من الأحيان.
- التكامل مع CI/CD: قم بأتمتة اختباراتك في خط أنابيب CI/CD الخاص بك لضمان اختبار جميع تغييرات التعليمات البرمجية بدقة قبل نشرها في الإنتاج. يمكن استخدام أدوات مثل Jenkins أو GitLab CI أو GitHub Actions أو CircleCI لتحقيق ذلك.
مثال: اختبار التدويل (i18n)
عند تطوير واجهات برمجة تطبيقات لجمهور عالمي، يعد التدويل (i18n) أمرًا ضروريًا. يتضمن اختبار i18n التحقق من أن واجهة برمجة التطبيقات الخاصة بك تدعم لغات ومناطق متعددة بشكل صحيح. إليك مثال على كيفية اختبار i18n في تطبيق FastAPI:
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
messages = {
"en": {"greeting": "Hello, world!"},
"fr": {"greeting": "Bonjour le monde !"},
"es": {"greeting": "¡Hola Mundo!"},
}
@app.get("/")
async def read_root(accept_language: Optional[str] = Header(None)):
lang = accept_language[:2] if accept_language else "en"
if lang not in messages:
lang = "en"
return {"message": messages[lang]["greeting"]}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root_en():
response = client.get("/", headers={"Accept-Language": "en-US"})
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_read_root_fr():
response = client.get("/", headers={"Accept-Language": "fr-FR"})
assert response.status_code == 200
assert response.json() == {"message": "Bonjour le monde !"}
def test_read_root_es():
response = client.get("/", headers={"Accept-Language": "es-ES"})
assert response.status_code == 200
assert response.json() == {"message": "¡Hola Mundo!"}
def test_read_root_default():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
يقوم هذا المثال بتعيين رأس Accept-Language لتحديد اللغة المطلوبة. ترجع واجهة برمجة التطبيقات التحية باللغة المحددة. يضمن الاختبار أن واجهة برمجة التطبيقات تتعامل بشكل صحيح مع تفضيلات اللغة المختلفة. إذا كان رأس Accept-Language غائبًا، فسيتم استخدام اللغة الافتراضية "en".
الخلاصة
الاختبار جزء أساسي من بناء تطبيقات FastAPI قوية وموثوقة. يوفر TestClient طريقة بسيطة ومريحة لاختبار نقاط نهاية API الخاصة بك. باتباع أفضل الممارسات الموضحة في هذا الدليل، يمكنك كتابة اختبارات شاملة تضمن جودة واستقرار واجهات برمجة التطبيقات الخاصة بك. من الطلبات الأساسية إلى التقنيات المتقدمة مثل حقن التبعية والاختبار غير المتزامن، يمكّنك TestClient من إنشاء تعليمات برمجية جيدة الاختبار وقابلة للصيانة. احتضن الاختبار كجزء أساسي من سير عمل التطوير الخاص بك، وستبني واجهات برمجة تطبيقات قوية وموثوقة للمستخدمين في جميع أنحاء العالم. تذكر أهمية تكامل CI/CD لأتمتة الاختبار وضمان ضمان الجودة المستمر.