کتابخانه pytest-asyncio در پایتون
با رشد برنامههای مبتنی بر asyncio در پایتون (وبفریمورکهایی مثل FastAPI، aiohttp یا کتابخانههایی مثل httpx)، نوشتن تستهای همآهنگ (asynchronous) به یک نیاز حیاتی تبدیل شده است. pytest-asyncio یک پلاگین برای pytest است که اجرای تابعهای async در تستها و استفاده از fixturesِ ناهمگام را ساده میکند. در این مقاله به نصب، الگوهای مرسوم، نکات پیشرفته و مقایسه با راهکارهای جایگزین میپردازیم.
نصب و آمادهسازی
برای نصب pytest-asyncio از pip استفاده کنید:
pip install pytest-asyncioپس از نصب، pytest بهطور خودکار پلاگین را بارگذاری میکند و میتوانید از marker و fixtures مربوطه استفاده کنید.
استفاده پایه — اجرای تستهای async
مهمترین و سادهترین کاربرد pytest-asyncio، اجرای توابع async بهعنوان تست است. کافی است از marker pytest.mark.asyncio استفاده کنید:
import asyncio
import pytest
async def add(a, b):
await asyncio.sleep(0.01)
return a + b
@pytest.mark.asyncio
async def test_add():
result = await add(2, 3)
assert result == 5در این مثال تابع async add با یک sleep کوتاه شبیهسازی شده و تست با pytest.mark.asyncio اجرا میشود. marker باعث میشود pytest یک حلقه رویداد برای اجرای coroutine بسازد و آن را اجرا کند.
fixtures ناهمگام (async fixtures)
pytest-asyncio اجازه میدهد fixtures نیز async باشند. این ویژگی برای آمادهسازی یا پاکسازی منابع ناهمگام (مثلاً اتصال به دیتابیس async یا راهاندازی سرور تست) ضروری است.
import pytest
import asyncpg
@pytest.fixture
async def db_conn():
conn = await asyncpg.connect(dsn="postgresql://user:pass@localhost/testdb")
yield conn
await conn.close()
@pytest.mark.asyncio
async def test_db_query(db_conn):
row = await db_conn.fetchrow("SELECT 1 AS val")
assert row["val"] == 1در این کد، fixture با کلیدواژه async def پیادهسازی شده و از yield برای پاکسازی استفاده میشود. تست میتواند fixture را بهصورت پارامتر دریافت کرده و مستقیماً await بزند.
fixtureهای با scope غیر از function (نکته مهم)
بهصورت پیشفرض event loop در pytest-asyncio تابعمحور (function-scoped) است، بنابراین اگر بخواهید fixture با scope=“session” یا “module” بسازید باید یک event_loop ویژه فراهم کنید:
import asyncio
import pytest
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.new_event_loop()
yield loop
loop.close()این fixture یک حلقه جدید برای سشن ایجاد میکند و اجازه میدهد fixtureهای session-scoped async یا تستهایی که نیاز به حلقه بلندمدت دارند اجرا شوند. دقت کنید که مدیریت صحیح باز و بسته شدن حلقه مهم است تا نشت منابع نداشته باشیم.
استفاده در پروژههای واقعی — مثال با httpx و FastAPI
اگر از FastAPI و httpx استفاده میکنید، میتوانید بهصورت زیر APIهای async را تست کنید:
from fastapi import FastAPI
from httpx import AsyncClient
import pytest
app = FastAPI()
@app.get("/ping")
async def ping():
return {"ping": "pong"}
@pytest.mark.asyncio
async def test_ping():
async with AsyncClient(app=app, base_url="http://test") as ac:
r = await ac.get("/ping")
assert r.status_code == 200
assert r.json() == {"ping": "pong"}در این کد از AsyncClient کتابخانه httpx با پارامتر app استفاده شده تا بدون نیاز به اجرای سرور واقعی endpointها تست شوند. این الگو مناسب تستهای endpoint و منطق بیزنسی ناهمگام است.
نکات پیشرفته و بهترین روشها
- برای تستهای با تاخیر زمانی طولانی، از
pytest.mark.timeoutیا تنظیمات pytest-timeout استفاده کنید تا تستها مسدود نشوند. - اگر از loopهای جایگزین مانند uvloop استفاده میکنید، میتوانید در
conftest.pyآن را ست کنید:import asyncio import uvloop def pytest_configure(): asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())این تنظیم باعث افزایش کارایی حلقه رویداد در تستها میشود.
- برای شبیهسازی شبکه یا پاسخهای HTTP ناهمگام از کتابخانههایی مثل aioresponses یا respx (برای httpx) استفاده کنید.
- در Mock کردن async functions از
AsyncMockدر کتابخانه unittest.mock (پایتون 3.8+) استفاده کنید تا رفتار awaitable بدرستی شبیهسازی شود.
مقایسه با pytest-anyio
| ویژگی | pytest-asyncio | pytest-anyio |
|---|---|---|
| پشتیبانی از backend | فقط asyncio | anyio (asyncio و trio و توابع مشترک) |
| سادگی | بسیار ساده برای پروژههای asyncio | مناسب پروژههایی که ممکن است Trio یا دیگر loopها را نیاز داشته باشند |
| استفاده در پروژههای موجود | بهترین انتخاب برای اکوسیستم asyncio | انعطافپذیرتر برای چند backend |
اشتباهات متداول و رفع آنها
- فراموش کردن
@pytest.mark.asyncio: تست async بدون marker اجرا نمیشود یا خطا میدهد. - استفاده از
asyncio.run()داخل تستهای pytest-asyncio: این کار ممکن است باعث تداخل با حلقه pytest شود؛ به جای آن از await مستقیم یا fixtureهای event_loop استفاده کنید. - تعریف session-scoped async fixture بدون فراهم کردن event_loop session-scoped: منجر به خطا میشود. راهحل ایجاد event_loop دستی (مطابق مثال) است.
نمونهای از تست پیشرفته با Mock و parametrize
import pytest
from unittest.mock import AsyncMock
class Service:
async def fetch(self, x):
return x * 2
@pytest.mark.asyncio
@pytest.mark.parametrize("input,expected", [(2,4),(3,6)])
async def test_service_fetch(monkeypatch, input, expected):
mock = AsyncMock(return_value=expected)
monkeypatch.setattr("path.to.module.Service.fetch", mock)
svc = Service()
result = await svc.fetch(input)
assert result == expected
mock.assert_awaited_once_with(input)این تست با استفاده از AsyncMock رفتار تابع async را شبیهسازی میکند و با monkeypatch جایگزینی انجام میدهد. سپس assert میکنیم تابع await شده و مقدار بازگشتی درست است.
نتیجهگیری کوتاه
pytest-asyncio ابزار قدرتمند و سادهای برای تست کدهای ناهمگام در پایتون است. با فراهم کردن marker برای اجرای test coroutineها و پشتیبانی از async fixtures، امکان تست واضح و ایزوله برای کدهای async فراهم میشود. برای پروژههای صرفاً asyncio این پلاگین اغلب بهترین انتخاب است، ولی اگر نیاز به پشتیبانی از backendهای دیگر دارید pytest-anyio انتخاب مناسبتری خواهد بود.
با رعایت نکات مربوط به scope و مدیریت event loop، تستهای پایدار و قابل اعتمادی خواهید داشت که اجرای CI و توسعه روزمره را آسان میکنند.
آیا این مطلب برای شما مفید بود ؟




