نحوه تست موثر برنامههای FastAPI را با TestClient بیاموزید. این راهنما شامل بهترین روشها، تکنیکهای پیشرفته و مثالهای واقعی برای ساخت APIهای قدرتمند و قابل اعتماد است.
تسلط بر تستنویسی در FastAPI: راهنمای جامع TestClient
FastAPI به عنوان یک فریمورک پیشرو برای ساخت APIهای با کارایی بالا با پایتون ظاهر شده است. سرعت، سهولت استفاده و اعتبارسنجی خودکار دادهها، آن را به یکی از محبوبترین فریمورکها در میان توسعهدهندگان در سراسر جهان تبدیل کرده است. با این حال، یک API خوب ساخته شده، تنها به اندازه تستهایش خوب است. تستهای جامع تضمین میکنند که API شما طبق انتظار عمل میکند، تحت فشار پایدار میماند و میتواند با اطمینان به محیط عملیاتی (production) مستقر شود. این راهنمای جامع بر استفاده از TestClient در FastAPI برای تست موثر نقاط پایانی (endpoints) API شما تمرکز دارد.
چرا تستنویسی برای برنامههای FastAPI اهمیت دارد؟
تستنویسی یک گام حیاتی در چرخه عمر توسعه نرمافزار است. این کار به شما کمک میکند:
- شناسایی زودهنگام باگها: خطاها را قبل از رسیدن به محیط عملیاتی شناسایی کرده و در زمان و منابع صرفهجویی کنید.
- تضمین کیفیت کد: کد خوشساختار و قابل نگهداری را ترویج دهید.
- جلوگیری از رگرسیون: تضمین کنید که تغییرات جدید، عملکرد موجود را مختل نمیکند.
- بهبود قابلیت اطمینان API: اعتماد به پایداری و عملکرد API را افزایش دهید.
- تسهیل همکاری: مستندات واضحی از رفتار مورد انتظار برای سایر توسعهدهندگان ارائه دهید.
معرفی TestClient در FastAPI
FastAPI یک TestClient داخلی ارائه میدهد که فرآیند تست نقاط پایانی API شما را ساده میکند. TestClient به عنوان یک کلاینت سبکوزن عمل میکند که میتواند بدون راهاندازی یک سرور کامل، درخواستهایی را به API شما ارسال کند. این امر تستنویسی را به طور قابل توجهی سریعتر و راحتتر میکند.
ویژگیهای کلیدی TestClient:
- شبیهسازی درخواستهای HTTP: به شما امکان میدهد درخواستهای GET، POST، PUT، DELETE و سایر درخواستهای HTTP را به API خود ارسال کنید.
- مدیریت سریالسازی دادهها: به طور خودکار دادههای درخواست (مانند payloadهای JSON) را سریالسازی و دادههای پاسخ را دیسریالسازی میکند.
- ارائه متدهای اَسرشِن (assertion): متدهای مناسبی را برای تأیید کد وضعیت (status code)، هدرها و محتوای پاسخها ارائه میدهد.
- پشتیبانی از تست ناهمگام: به طور یکپارچه با ماهیت ناهمگام 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 ذخیره کنید. این برنامه سه نقطه پایانی (endpoint) را تعریف میکند:
/: یک نقطه پایانی GET ساده که پیام "Hello World" را برمیگرداند./items/{item_id}: یک نقطه پایانی GET که یک آیتم را بر اساس ID آن برمیگرداند./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و نمونهappFastAPI را ایمپورت میکنیم. - یک نمونه از
TestClientرا با ارسالappایجاد میکنیم. - یک تابع تست به نام
test_read_rootتعریف میکنیم. - در داخل تابع تست، از
client.get("/")برای ارسال یک درخواست GET به نقطه پایانی ریشه استفاده میکنیم. - تأیید میکنیم که کد وضعیت پاسخ 200 (OK) است.
- تأیید میکنیم که JSON پاسخ برابر با
{"message": "Hello World"}است.
اجرای تستها با pytest
برای اجرای تستهای خود، به سادگی یک ترمینال را در دایرکتوری حاوی فایل 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، باید دادهها را در بدنه درخواست (request body) ارسال کنید. 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را به عنوان payload 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
به خاطر داشته باشید که برای تأیید اینکه پاسخها طبق انتظار هستند، اَسرشِنها (assertions) را اضافه کنید.
تکنیکهای پیشرفته تستنویسی
TestClient چندین ویژگی پیشرفته ارائه میدهد که میتواند به شما در نوشتن تستهای جامعتر و موثرتر کمک کند.
تست با وابستگیها (Dependencies)
سیستم تزریق وابستگی (dependency injection) FastAPI به شما امکان میدهد تا به راحتی وابستگیها را به نقاط پایانی API خود تزریق کنید. هنگام تستنویسی، ممکن است بخواهید این وابستگیها را بازنویسی (override) کنید تا پیادهسازیهای Mock یا مختص تست ارائه دهید.
به عنوان مثال، فرض کنید برنامه شما به یک اتصال پایگاه داده وابسته است. میتوانید وابستگی پایگاه داده را در تستهای خود بازنویسی کنید تا از یک پایگاه داده در حافظه (in-memory) استفاده کنید:
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 را با یک تابع مخصوص تست که یک سشن (session) را به یک پایگاه داده SQLite در حافظه برمیگرداند، بازنویسی میکند. مهم: ایجاد متادیتا باید به صراحت برای کارکرد صحیح پایگاه داده تست فراخوانی شود. عدم ایجاد جدول منجر به خطاهای مربوط به جداول از دست رفته خواهد شد.
تست کد ناهمگام (Asynchronous)
FastAPI به گونهای ساخته شده است که ناهمگام (asynchronous) باشد، بنابراین شما اغلب نیاز به تست کد ناهمگام خواهید داشت. 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.mark.asyncio، باید pytest-asyncio را نصب کنید: pip install pytest-asyncio. همچنین اگر از نسخههای قدیمیتر pytest استفاده میکنید، باید مطمئن شوید که asyncio.get_event_loop() پیکربندی شده است. در صورت استفاده از pytest نسخه 8 یا جدیدتر، این مورد ممکن است لازم نباشد.
تست آپلود فایل
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 یک فایل dummy (ساختگی) ایجاد کرده و آن را به پارامتر files ارسال میکنیم. پارامتر files لیستی از تاپلها را میپذیرد که هر تاپل شامل نام فیلد، نام فایل و محتوای فایل است. نوع محتوا (content type) برای مدیریت دقیق توسط سرور مهم است.
تست مدیریت خطا
مهم است که نحوه مدیریت خطاها در API خود را تست کنید. میتوانید از TestClient برای ارسال درخواستهای نامعتبر استفاده کرده و تأیید کنید که API پاسخهای خطای صحیح را برمیگرداند.
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 پاسخ حاوی پیام خطای مورد انتظار است.
تست ویژگیهای امنیتی
اگر API شما از احراز هویت (authentication) یا مجوز (authorization) استفاده میکند، باید این ویژگیهای امنیتی را نیز تست کنید. 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"}
در این مثال، ما نقطه پایانی ورود را تست میکنیم و سپس از توکن دریافت شده برای دسترسی به یک مسیر محافظت شده استفاده میکنیم. پارامتر headers متدهای درخواست TestClient به شما امکان میدهد هدرهای سفارشی، از جمله هدر Authorization برای توکنهای Bearer را تنظیم کنید.
بهترین روشها برای تستنویسی FastAPI
در اینجا برخی از بهترین روشها برای پیگیری هنگام تست برنامههای FastAPI شما آورده شده است:
- تستهای جامع بنویسید: برای پوشش بالای تست (test coverage) تلاش کنید تا اطمینان حاصل شود که تمام قسمتهای API شما به طور کامل تست شدهاند.
- از نامهای توصیفی برای تستها استفاده کنید: مطمئن شوید که نام تستهای شما به وضوح نشان میدهد که تست در حال تأیید چه چیزی است.
- الگوی Arrange-Act-Assert را دنبال کنید: تستهای خود را به سه مرحله متمایز سازماندهی کنید: Arrange (تنظیم دادههای تست)، Act (انجام عملی که در حال تست است) و Assert (تأیید نتایج).
- از اشیاء Mock استفاده کنید: وابستگیهای خارجی را Mock کنید تا تستهای خود را ایزوله کرده و از وابستگی به سیستمهای خارجی اجتناب کنید.
- موارد مرزی (edge cases) را تست کنید: API خود را با ورودیهای نامعتبر یا غیرمنتظره تست کنید تا اطمینان حاصل شود که خطاها را به درستی مدیریت میکند.
- تستها را مکرراً اجرا کنید: تستنویسی را در گردش کار توسعه خود ادغام کنید تا باگها را در اوایل و به دفعات شناسایی کنید.
- با CI/CD یکپارچه شوید: تستهای خود را در خط لوله CI/CD خود خودکار کنید تا اطمینان حاصل شود که تمام تغییرات کد قبل از استقرار در محیط عملیاتی به طور کامل تست شدهاند. ابزارهایی مانند Jenkins، GitLab CI، GitHub Actions یا CircleCI میتوانند برای دستیابی به این هدف استفاده شوند.
مثال: تست بینالمللیسازی (i18n)
هنگام توسعه APIها برای مخاطبان جهانی، بینالمللیسازی (i18n) ضروری است. تست i18n شامل تأیید این است که API شما از چندین زبان و منطقه به درستی پشتیبانی میکند. در اینجا مثالی از نحوه تست 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 را برای مشخص کردن زبان مورد نظر تنظیم میکند. API تبریک را به زبان مشخص شده برمیگرداند. تستنویسی تضمین میکند که API به درستی اولویتهای زبان مختلف را مدیریت میکند. اگر هدر Accept-Language وجود نداشته باشد، از زبان پیشفرض "en" استفاده میشود.
نتیجهگیری
تستنویسی بخش اساسی ساخت برنامههای FastAPI قدرتمند و قابل اعتماد است. TestClient راهی ساده و راحت برای تست نقاط پایانی API شما فراهم میکند. با دنبال کردن بهترین روشهای ذکر شده در این راهنما، میتوانید تستهای جامعی بنویسید که کیفیت و پایداری APIهای شما را تضمین کند. از درخواستهای پایه گرفته تا تکنیکهای پیشرفتهای مانند تزریق وابستگی و تست ناهمگام، TestClient شما را قادر میسازد تا کدی با تستهای خوب و قابل نگهداری ایجاد کنید. تستنویسی را به عنوان بخش اصلی گردش کار توسعه خود بپذیرید، و APIهایی خواهید ساخت که هم قدرتمند و هم قابل اعتماد برای کاربران در سراسر جهان هستند. اهمیت یکپارچگی CI/CD برای خودکارسازی تستنویسی و تضمین مداوم کیفیت را به خاطر بسپارید.