ویژگی تصویر

کتابخانه pytest-asyncio در پایتون — راهنمای کامل برای تست‌های async

  /  پایتون   /  کتابخانه 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-asynciopytest-anyio
پشتیبانی از backendفقط asyncioanyio (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 و توسعه روزمره را آسان می‌کنند.

آیا این مطلب برای شما مفید بود ؟

خیر
بله
موضوعات شما در انجمن: