FA-TOOLS — Header Component

آموزش pytest: تست‌نویسی حرفه‌ای در پایتون

فهرست مطالب

خلاصه سریع

تست‌نویسی خودکار تضمین‌کننده کیفیت و پایداری نرم‌افزار است. فریم‌ورک pytest محبوب‌ترین ابزار تست‌نویسی در پایتون است که با حذف کدهای اضافی (Boilerplate)، ابزار قدرتمند Fixture و پشتیبانی از پلاگین‌های متعدد، فرآیند سنجش صحت کد را بسیار ساده و سریع می‌کند. در این راهنمای جامع، تمام ابزارها و استراتژی‌های لازم برای نوشتن تست‌های واحد (Unit Test) و تکاملی به‌صورت کاملاً عملی آموزش داده شده است.

تست‌نویسی خودکار بخش جدایی‌ناپذیر از توسعه نرم‌افزار مدرن است. استفاده از اسنیپت‌های پایتون در پروژه‌های بزرگ بدون داشتن تست‌های مطمئن، خطرات شکست برنامه‌ها در محیط تولید را به‌شدت افزایش می‌دهد. فریم‌ورک pytest به شما کمک می‌کند تا تست‌هایی خوانا، مقیاس‌پذیر و قابل نگهداری بنویسید.

۱. pytest چیست و چرا باید از آن استفاده کنیم؟

آموزش pytest: تست‌نویسی حرفه‌ای در پایتون — تصویر 1

پاسخ سریع: pytest یک فریم‌ورک تست‌نویسی قدرتمند و بدون کد زائد برای پایتون است که ساخت تست‌های ساده و پیچیده را راحت می‌کند. این فریم‌ورک با پشتیبانی از دستور ساده assert، مدیریت هوشمند پیش‌نیازها با Fixture و زیست‌بوم غنی پلاگین‌ها، جایگزین برتر ماژول استاندارد unittest است.

ماژول پیش‌فرض پایتون برای تست‌نویسی، unittest است که معماری خود را از زبان جاوا و الگوریتم‌های Object-Oriented قدیمی وام گرفته است. این مسئله باعث می‌شود برای نوشتن ساده‌ترین تست‌ها مجبور به تعریف کلاس‌ها و استفاده از متدهای خاصی مانند assertEqual باشید. در مقابل، pytest رویکردی کاملاً پایتونیک دارد.

معیار مقایسه تفاوت pytest و unittest
سینتکس و نحوه نگارش استفاده از توابع ساده پایتون و دستور استاندارد assert در برابر الزام به ساخت کلاس و متدهای اختصاصی.
مدیریت پیش‌نیازها (Setup/Teardown) سیستم قدرتمند Fixture با اسکوپ‌های تزریق وابسته در برابر متدهای محدود setUp و tearDown.
ورودی‌های متغیر (Parametrization) پشتیبانی داخلی و بسیار ساده از دکوراتور parametrize در برابر پیچیدگی بالا در unittest.
اکوسیستم پلاگین‌ها دارای بیش از ۳۰۰ پلاگین فعال برای تست‌های Async، Coverage، HTML Report و…

۲. نصب و اجرای اولین تست در pytest

آموزش pytest: تست‌نویسی حرفه‌ای در پایتون — تصویر 2

برای شروع کار با pytest، ابتدا باید آن را از طریق مدیریت پکیج پایتون (pip) نصب کنید. توصیه می‌شود همیشه تست‌های خود را در یک محیط مجازی (Virtual Environment) اجرا کنید.

pip install pytest

حالا یک فایل به نام test_calculator.py بسازید و تابع ساده زیر را در آن بنویسید:

def add(a, b):
    return a + b

def test_add_numbers():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

برای اجرای تست، کافی است ترمینال را در مسیر فایل باز کرده و دستور زیر را اجرا کنید:

pytest

۳. ساختار و قراردادهای نام‌گذاری

آموزش pytest: تست‌نویسی حرفه‌ای در پایتون — تصویر 3

pytest به‌صورت خودکار فایل‌ها و کدهای تست را در پروژه پیدا می‌کند (Test Discovery). برای اینکه ابزار بتواند کدهای شما را بشناسد، رعایت قراردادهای نام‌گذاری زیر الزامی است:

  • نام فایل‌ها: باید به‌صورت test_*.py یا *_test.py باشد.
  • نام توابع: باید با پیشوند test_ شروع شوند (مثلا test_login_success).
  • نام کلاس‌ها: باید با پیشوند Test شروع شود و شامل متد __init__ نباشد (مثلا TestUserAuth).

۴. مدیریت Assertionها و بررسی استثناها

آموزش pytest: تست‌نویسی حرفه‌ای در پایتون — تصویر 4

برخلاف unittest که نیازمند حفظ کردن ده‌ها متد مثل assertTrue یا assertIsInstance است، در pytest فقط از کلمه کلیدی assert استفاده می‌کنید. pytest با تکنیک بازنویسی AST، هنگام مواجهه با خطا، اطلاعات بسیار دقیقی از مقدار متغیرها ارائه می‌دهد.

تست رخ دادن استثناها (Exceptions)

گاهی نیاز دارید اطمینان حاصل کنید که کد شما در صورت دریافت ورودی اشتباه، خطای مناسبی صادر می‌کند. برای این کار از pytest.raises استفاده می‌شود:

import pytest

def divide(a, b):
    if b == 0:
        raise ValueError("تقسیم بر صفر امکان‌پذیر نیست")
    return a / b

def test_divide_by_zero():
    with pytest.raises(ValueError) as exc_info:
        divide(10, 0)
    
    assert "تقسیم بر صفر" in str(exc_info.value)

۵. فیکسچرها (Fixtures): مدیریت پیش‌نیازها و داده‌ها

آموزش pytest: تست‌نویسی حرفه‌ای در پایتون — تصویر 5

پاسخ سریع: فیکسچرها (Fixtures) توابعی هستند که محیط و داده‌های پیش‌نیاز برای اجرای تست (مثل اتصال به دیتابیس، ساخت کاربر مجازی یا تنظیمات اولیه) را آماده می‌کنند. آنها با تزریق وابستگی به توابع تست متصل می‌شوند و کد را تمیز و ماژولار نگه می‌دارند.

فیکسچرها با دکوراتور @pytest.fixture تعریف می‌شوند. نام فیکسچر مستقیماً به عنوان پارامتر ورودی به تابع تست پاس داده می‌شود:

import pytest

@pytest.fixture
def sample_user():
    return {"username": "ali", "email": "ali@example.com", "role": "admin"}

def test_user_role(sample_user):
    assert sample_user["role"] == "admin"

عملیات پاک‌سازی (Teardown) با yield

اگر نیاز دارید بعد از اتمام تست، منابع اشغال شده (مثل بسته شدن فایل یا دیتابیس) آزاد شوند، به جای return از yield استفاده کنید:

@pytest.fixture
def db_connection():
    # ۱. راه اندازی (Setup)
    db = connect_to_db()
    yield db
    # ۲. پاک‌سازی (Teardown)
    db.close()

طول عمر فیکسچرها (Scope)

شما می‌توانید طول عمر بازسازی فیکسچر را مدیریت کنید تا از اجرای تکراری کدهای سنگین جلوگیری شود:

  • scope="function": اجرا به ازای هر تابع تست (پیش‌فرض).
  • scope="class": یک‌بار اجرا به ازای هر کلاس تست.
  • scope="module": یک‌بار اجرا به ازای کل فایل تست.
  • scope="session": یک‌بار اجرا در کل فرآیند اجرای pytest.

۶. پارامترسازی (Parametrization): جلوگیری از تکرار کد

اگر می‌خواهید یک تابع تست را با ورودی‌ها و خروجی‌های مختلف اجرا کنید، نیازی به نوشتن چند تابع جداگانه نیست. دکوراتور @pytest.mark.parametrize این کار را به‌سادگی انجام می‌دهد:

import pytest

def is_even(number):
    return number % 2 == 0

@pytest.mark.parametrize("num, expected", [
    (2, True),
    (3, False),
    (0, True),
    (-4, True),
    (-5, False),
])
def test_is_even(num, expected):
    assert is_even(num) == expected

۷. ماک‌کردن (Mocking) و شبیه‌سازی وابستگی‌ها

هنگام تست کدهایی که به سرویس‌های خارجی (مثل API‌های اینترنتی یا درگاه پرداخت) وابسته‌اند، نباید درخواست واقعی ارسال کنید. برای این کار از پلاگین pytest-mock و فیکسچر mocker استفاده می‌شود.

ابتدا پکیج مربوطه را نصب کنید: pip install pytest-mock

import requests

def get_ip():
    response = requests.get("https://api.ipify.org?format=json")
    return response.json()["ip"]

def test_get_ip(mocker):
    # شبیه‌سازی پاسخ کتابخانه requests
    mock_get = mocker.patch("requests.get")
    mock_get.return_value.json.return_value = {"ip": "127.0.0.1"}

    ip = get_ip()
    
    assert ip == "127.0.0.1"
    mock_get.assert_called_once_with("https://api.ipify.org?format=json")

۸. تست‌نویسی کدهای Asynchronous

اگر در پروژه خود از asyncio استفاده می‌کنید، برای تست توابع asynchronous نیازمند پلاگین pytest-asyncio هستید. پس از نصب (pip install pytest-asyncio)، می‌توانید تست‌های هم‌روند بنویسید:

import pytest
import asyncio

async def fetch_data():
    await asyncio.sleep(0.1)
    return "Data"

@pytest.mark.asyncio
async def test_fetch_data():
    result = await fetch_data()
    assert result == "Data"

۹. فایل conftest.py و ساختار استاندارد پروژه

فایل conftest.py یکی از ویژگی‌های قدرتمند pytest است. فیکسچرهایی که در این فایل تعریف می‌شوند، به‌صورت اشتراکی در تمام فایل‌های تست پروژه بدون نیاز به import قابل استفاده هستند.

ساختار استاندارد یک پروژه پایتون مجهز به تست به‌صورت زیر است:

my_project/
│
├── my_app/
│   ├── __init__.py
│   ├── auth.py
│   └── models.py
│
└── tests/
    ├── __init__.py
    ├── conftest.py
    ├── test_auth.py
    └── test_models.py

اگر به مطالعه گزیده‌های بیشتر در زمینه ساختار پروژه علاقه‌مندید، بخش کدهای آماده و اسنیپت‌ها را مشاهده کنید.

۱۰. اندازه‌گیری پوشش کد (Code Coverage)

پوشش کد مشخص می‌کند چند درصد از کدهای برنامه شما توسط تست‌ها اجرا و سنجیده شده‌اند. با نصب پلاگین pytest-cov می‌توانید گزارش‌های دقیقی دریافت کنید:

pip install pytest-cov
pytest --cov=my_app --cov-report=html

دستور بالا یک پوشه به نام htmlcov می‌سازد که با باز کردن فایل index.html در مرورگر، می‌توانید خطوطی از کد که تست نشده‌اند را به‌صورت گرافیکی مشاهده کنید.

۱۱. ۳ نکته کلیدی و اشتباهات رایج در تست‌نویسی با pytest

۱. نشت حالت (State Leakage) بین تست‌ها در فیکسچرهای Mutable

اگر یک فیکسچر مقدار قابل تغییر (مثل dict یا list) برگرداند و اسکوپ آن module یا session باشد، تغییر داده‌ها در یک تست روی سایر تست‌ها اثر می‌گذارد. همیشه برای داده‌های Mutable از اسکوپ function استفاده کنید.

۲. ماک‌کردن در محل استفاده (Where to Patch)

اشتباه رایج توسعه‌دهندگان، ماک‌کردن ماژول در محل تعریف آن است. همواره ماژول را در **محلی که فراخوانی می‌شود** ماک کنید. به عنوان مثال، اگر auth.py از requests استفاده می‌کند، باید my_app.auth.requests.get را ماک کنید، نه خود requests.get را.

۳. نوشتن تست‌های وابسته به ترتیب اجرا

تست‌های واحد باید ایزوله و مستقل باشند. اگر اجرای تست B مشروط به اجرای قبلی تست A باشد، معماری تست اشتباه است. تست‌ها باید در هر ترتیبی (حتی به‌صورت رندوم یا موازی) نتیجه یکسانی بدهند.

۱۲. عیب‌یابی سریع مشکلات رایج

مشکل ۱: خطای ModuleNotFoundError هنگام اجرای pytest

علت: پایتون مسیر پروژه را به sys.path اضافه نکرده است.
راه حل: تست‌ها را به جای دستور pytest با python -m pytest اجرا کنید تا مسیر جاری به سورس شناسایی شود.

مشکل ۲: شناسایی نشدن تست‌های جدید توسط pytest

علت: عدم رعایت نام‌گذاری استاندارد.
راه حل: مطمئن شوید نام فایل با test_ شروع می‌شود و توابع داخل آن نیز پیشوند test_ دارند.

مشکل ۳: عدم نمایش پرینت‌ها (print) در خروجی

علت: pytest به‌صورت پیش‌فرض خروجی stdout را ضبط (Capture) می‌کند.
راه حل: هنگام اجرای تست سوییچ -s را اضافه کنید: pytest -s

۱۳. پرسش‌های متداول

آیا می‌توان تست‌های نوشته شده با unittest را در pytest اجرا کرد؟

بله، pytest کاملا با تست‌های ساختار unittest سازگار است و بدون نیاز به تغییر کد می‌توانید آنها را اجرا کنید.

تفاوت اصلی yield و return در Fixtureهای pytest چیست؟

دستور return فقط داده را برمی‌گرداند، اما yield امکان اجرای کدهای پاک‌سازی (Teardown) را پس از اتمام تست فراهم می‌سازد.

چگونه می‌توانیم فقط یک تست خاص را اجرا کنیم؟

با استفاده از مسیر دقیق فایل و نام تابع: pytest tests/test_auth.py::test_login

پلاگین pytest-xdist چیست و چه کاربردی دارد؟

این پلاگین به شما اجازه می‌دهد تست‌ها را به‌صورت موازی روی چند هسته CPU اجرا کنید تا زمان اجرای تست‌های سنگین به‌شدت کاهش یابد.

سوالات متداول

تفاوت اصلی pytest با unittest در چیست؟

pytest با سینتکس پایتونیک و استفاده از دستور ساده assert نیاز به نوشتن کلاس و کد زائد را از بین می‌برد، در حالی که unittest ساختار شئ‌گرا و پیچیده‌تری دارد.

فیکسچر (Fixture) در pytest چه کاربردی دارد؟

فیکسچرها توابعی برای مدیریت پیش‌نیازها مانند اتصال به دیتابیس یا ساخت داده‌های اولیه هستند که کد تست را ماژولار و تمیز نگه می‌دارند.

چگونه تست‌های تکراری با ورودی‌های مختلف بنویسیم؟

با استفاده از دکوراتور parametrize می‌توانید یک تابع تست را با مقادیر و ورودی‌های متغیر به صورت خودکار و بدون تکرار کد اجرا کنید.

فایل conftest.py چه وظیفه‌ای در پروژه دارد؟

این فایل برای اشتراک‌گذاری فیکسچرها و تنظیمات عمومی بین تمام فایل‌های تست بدون نیاز به ایمپورت دستی استفاده می‌شود.

Table of Contents

آخرین نوشته‌ها

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *