تا اینجای کار یاد گرفتیم که چطور با تنظیم محدوده عمر فیکسچرها، کنترل حافظه و سرعت اجرای تست‌ها را به دست بگیریم. اما با بزرگ‌تر شدن پروژه و افزایش تعداد فایل‌های تست، با یک چالش جدید مواجه می‌شویم. اگر بخواهیم از یک فیکسچر مشترک در چندین فایل مجزا استفاده کنیم، مجبوریم آن کدها را در هر فایل کپی کنیم یا مدام درگیر ایمپورت‌های طولانی و پیچیده پایتون شویم؛ روشی که ساختار پروژه را شلوغ و مدیریت آن را سخت می‌کند.

در این درس یاد می‌گیرید که چطور با استفاده از یک فایل جادویی و مرکزی به نام conftest.py، تمام فیکسچرهای پروژه را یک‌جا جمع کنید و بدون نیاز به حتی یک خط ایمپورت، آن‌ها را به صورت خودکار در تمام توابع تست به اشتراک بگذارید. این کار فرآیند تمیزکاری کدهای تست را به اوج سادگی و بهره‌وری می‌رساند.

اما داستان به همین‌جا ختم نمی‌شود. یک سیستم احراز هویت یا مدیریت مالی ایده‌آل، علاوه بر بررسی رفتارهای درست، باید بتواند در برابر رفتارهای اشتباه و خطاها هم واکنش درستی نشان دهد. به همین دلیل در بخش دوم این درس، سراغ مدیریت استثناها می‌رویم و یاد می‌گیریم که چطور با ابزار pytest.raises تست خطاهای پیشرفته را بنویسیم تا مطمئن شویم سیستم در مواجهه با نام کاربری تکراری یا برداشت‌های غیرمجاز، دقیقاً همان خطایی را پرتاب می‌کند که ما انتظارش را داریم.

بحران کدهای تکراری با بزرگ شدن ساختار پروژه

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

کپی کردن فیکسچرها شاید در روزهای اول کار شما را راه بیندازد، اما به مرور زمان یک بحران بزرگ به نام شلوغ شدن معماری پروژه ایجاد می‌کند.

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

اصل حرف این است که تمیزکاری کدهای تست با Fixtures نباید خودش باعث شلوغی و شلختگی کدهای شما شود. ما به یک مکانیزم هوشمند برای اشتراک‌گذاری فیکسچرها نیاز داریم؛ ساختاری که اجازه دهد فیکسچرهای عمومی و پرکاربرد را فقط یک بار در یک مکان امن بنویسیم و بقیه فایل‌های تست بدون کپی‌کاری به آن دسترسی داشته باشند. در بخش بعدی با فایل نجات‌دهنده‌ای آشنا می‌شویم که این بحران را در فریم‌ورک pytest برای همیشه حل می‌کند.

فایل conftest.py چیست و چطور معجزه می‌کند؟

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

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

فرض کنید یک فیکسچر سنگین برای شبیه‌سازی اتصال به دیتابیس یا تنظیمات اولیه سیستم مدیریت کاربران دارید. به جای اینکه این کدها را در تک‌تک فایل‌های مربوط به کیف پول یا بخش احراز هویت کپی کنید، آن را یک بار درون این هاب مرکزی می‌نویسید. با این کار، اصل مستقل بودن تست‌ها کاملاً حفظ می‌شود، اما کدهای آماده‌سازی به شدت تمیز و یکپارچه خواهند شد.

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

انتقال فیکسچرهای مدیریت کاربران و کیف پول به فایل مرکزی

وقت آن رسیده که دست به کد شویم و این هاب مرکزی را بسازیم. یک فایل جدید دقیقاً در ریشه پروژه پایتون خود به نام conftest.py ایجاد کنید. حالا فیکسچرهای مربوط به کیف پول و سیستم مدیریت کاربران را از فایل‌های قبلی برمی‌داریم و به این فایل منتقل می‌کنیم.

کدهای زیر را درون فایل conftest.py قرار دهید:

import pytest
from wallet import Wallet
from users import UserManager

@pytest.fixture
def empty_wallet():
    return Wallet()

@pytest.fixture
def wallet_with_funds():
    return Wallet(initial_amount=100)

@pytest.fixture(scope="module")
def user_manager_system():
    manager = UserManager()
    manager.register_user("samandar", role="admin")
    yield manager
    manager.users.clear()

ببین چقدر تمیز شد؛ اکنون تمام ابزارهای آماده‌سازی محیط تست در یک پناهگاه امن مستقر شده‌اند. نکته جالب اینجاست که فیکسچر user_manager_system هنوز همان اسکوپ ماژول را دارد و کدهای پاک‌سازی بعد از yield هم سر جایشان هستند. فرآیند تمیزکاری کدهای تست با Fixtures با این فایل مرکزی، مدیریت پروژه را بسیار آسان می‌کند.

حالا برای اینکه مطمئن شویم اشتراک‌گذاری فیکسچرها بدون مشکل کار می‌کند، فایل کدهای تست مربوط به کاربران یعنی test_users.py را باز کنید و آن را به این صورت تغییر دهید:

# در این فایل دیگر هیچ فیکسچری تعریف نشده و هیچ ایمپورتی برای فیکسچر نداریم

def test_admin_creation(user_manager_system):
    assert user_manager_system.get_user_count() == 1
    assert user_manager_system.users["samandar"]["role"] == "admin"

def test_add_regular_user(user_manager_system):
    user_manager_system.register_user("ali")
    assert user_manager_system.get_user_count() == 2

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

تست خطاهای پیشرفته با pytest.raises

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

برای ارزیابی این رفتارها، فریم‌ورک از ابزاری به نام pytest.raises استفاده می‌کند. این ابزار مانند یک تور مهارکننده عمل می‌کند؛ به این صورت که کدی که احتمال می‌دهیم خطا تولید کند را درون آن قرار می‌دهیم. اگر کد واقعاً خطا بدهد، تست پاس می‌شود و اگر خطا ندهد، تست رد خواهد شد.

بیا یک تست برای سناریوی خطای تکراری بودن نام کاربری در فایل test_users.py بنویسیم:

import pytest

def test_duplicate_user_error(user_manager_system):
    with pytest.raises(ValueError) as exc_info:
        user_manager_system.register_user("samandar")
    
    assert str(exc_info.value) == "این نام کاربری قبلاً ثبت شده است."

در این کدهای پایتون، از دستور with استفاده کردیم تا یک محیط تحت نظر برای مدیریت استثناها بسازیم. کلمه کلیدی as exc_info به ما اجازه می‌دهد به جزییات خطای پرتاب‌شده دسترسی داشته باشیم. در خط آخر، با یک دستور assert ساده مطمئن می‌شویم که متن پیام خطا دقیقاً همان چیزی است که در کلاس اصلی نوشته بودیم.

بذار ساده بگم؛ اگر متد ثبت‌نام به هر دلیلی خطایی غیر از ValueError پرتاب کند، یا اصلاً هیچ خطایی ندهد، ابزار ارزیابی نرم‌افزار متوجه این رفتار اشتباه می‌شود و تست را قرمز می‌کند. این شیوه از تست خطاهای پیشرفته به شما ضمانت می‌دهد که کدهای مدیریت بحران سیستم شما در خط تولید کاملاً درست کار می‌کنند.

در بخش بعدی، یاد می‌گیریم که چطور با تکنیک تزریق فیکسچر به توابع ارزیابی خطا، نمونه‌های ساخته‌شده در فایل مرکزی را مستقیماً وارد محیط مدیریت استثناها کنیم و کدهای تست هوشمندتری بنویسیم.

تزریق فیکسچر به توابع ارزیابی خطا

حالا پازل این درس را با ترکیب دو ابزار قدرتمند کامل می‌کنیم. می‌خواهیم ببینیم چطور فیکسچرهای موجود در هاب مرکزی را مستقیماً وارد توابعی کنیم که وظیفه آن‌ها ارزیابی خطاهاست. این تکنیک به ما اجازه می‌دهد بدون نیاز به ساختن دستی نمونه‌های جدید، رفتارهای بحرانی سیستم را در شرایط واقعی بسنجیم.

بیا سراغ فایل کدهای تست کیف پول یعنی test_wallet.py برویم. می‌خواهیم سناریویی را ارزیابی کنیم که در آن کاربر تمایل دارد مبلغی بیشتر از موجودی فعلی خود برداشت کند. این کار باید منجر به یک خطای مالی مشخص شود.

کدهای زیر را به فایل test_wallet.py اضافه کن:

import pytest
from wallet import InsufficientFunds

def test_withdraw_more_than_balance_error(wallet_with_funds):
    with pytest.raises(InsufficientFunds) as exc_info:
        wallet_with_funds.withdraw(amount=150)
    
    assert "موجودی کافی نیست" in str(exc_info.value)

در این تابع تست، فیکسچر wallet_with_funds را که قبلاً در فایل مرکزی تعریف کرده بودیم، به عنوان ورودی تزریق کردیم. این فیکسچر یک کیف پول آماده با ۱۰۰ واحد موجودی به ما تحویل می‌دهد. سپس با استفاده از ساختار مدیریت استثناها، تلاش می‌کنیم تا ۱۵۰ واحد از آن برداشت کنیم.

اصل حرف این است که تزریق فیکسچر به توابع ارزیابی خطا به کدهای تست شما هویت می‌بخشد. شما دیگر نیازی ندارید برای تست کردن حالت‌های خطا، از صفر دیتاسازی کنید. فیکسچر مرکزی محیط را آماده می‌کند، ابزار pytest.raises خطای پرتاب‌شده را شکار می‌کند و در نهایت دستور assert مهر تأیید را روی تست می‌زند.

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