نگاهی به توابع تستی که تا اینجای کار برای پروژه کیف پول دیجیتال نوشتهاید بیندازید. یک الگوی تکراری و آزاردهنده در خطوط اولیه تمام آنها به چشم میخورد: نمونهسازی مداوم از کلاس اصلی. در شروع هر سناریو، مجبور بودهایم خطی مثل wallet = DigitalWallet() را بنویسیم تا یک آبجکت تازه با دارایی اولیه در اختیار داشته باشیم.
این فرآیند راهاندازی تکراری، شاید در پروژههای کوچک به چشم نیاید، اما با بزرگ شدن پوشه تست، کدهای شما را سنگین، شلوغ و مستعد خطاهای ناخواسته میکند.
توسعهدهندگان ارشد پایتون هرگز وقت خود را تلف نوشتن این کدهای تکراری برای آمادهسازی محیط ارزیابی نرمافزار نمیکنند. اینجاست که تکنیک تمیزکاری کدهای تست با Fixtures به عنوان یک راهکار نجاتبخش وارد بازی میشود. فریمورک یک مکانیزم بومی و فوقالعاده هوشمند به نام فیکسچرهای pytest در اختیارتان میگذارد که وظیفهاش مدیریت وابستگیها و تزریق خودکار نمونههای آماده به توابع ارزیابی است.
فرض کنید یک دستیار هوشمند در پشت صحنه پروژه دارید؛ این دستیار قبل از اجرای هر تابع، یک آبجکت کیف پول تمیز، ایزوله و آماده به کار را روی میز کار شما میگذارد و به محض اتمام فرآیند عیبیابی، محیط را برای سناریوی بعدی پاکسازی میکند.
در بخش اول این درس، یاد میگیرید که چطور با استفاده از دکوراتور @pytest.fixture از شر کدهای راهاندازی اضافه خلاص شوید، خوانایی پوشه تست خود را به اوج برسانید و یک زیرساخت مقیاسپذیر برای کدهای مالی خود بسازید. ادیتور خود را باز کنید تا فرآیند بهینهسازی کدهای پایتون را شروع کنیم.
مشکل پنهان در راهاندازیهای مکرر (Setup Duplication)
بگذارید فایل test_wallet.py را یک بار دیگر با هم مرور کنیم و ببینیم تا اینجای کار چه کدهایی نوشتهایم. اگر به کدهای ارزیابی متدهای واریز، برداشت یا بخش صحتسنجی خطاها نگاه کنید، یک نقطه مشترک در شروع تمام آنها میبینید:
def test_deposit():
wallet = DigitalWallet(owner="سهراب", balance=100.0) # کد تکراری
wallet.deposit(50.0)
assert wallet.balance == 150.0
def test_withdraw_error():
wallet = DigitalWallet(owner="سهراب", balance=100.0) # باز هم کد تکراری
with pytest.raises(ValueError):
wallet.withdraw(200.0)
در شروع هر تابع، ما عملاً داریم یک خط کد کاملاً کپیپست شده را اجرا میکنیم تا نمونهسازی مداوم از کلاس اصلی پروژه را انجام دهیم. به این وضعیت در مهندسی نرمافزار، تکرار فرآیند راهاندازی یا Setup Duplication میگویند. شاید در نگاه اول این کار بیضرر به نظر برسد، اما یک مشکل پنهان و خطرناک در خود دارد.
فرض کنید فردا تصمیم بگیرید متد سازنده کلاس را تغییر دهید؛ مثلاً پارامتر اجباری دیگری مثل نوع ارز (Currency) را به کلاس کیف پول دیجیتال اضافه کنید. در این حالت، معماری تستهای شما فرو میریزد. شما مجبور میشوید تکتک توابع تست را به صورت دستی باز کنید و خط اول آنها را ویرایش کنید.
تکرار این کدهای آمادهسازی، خوانایی پوشه تست را به شدت کاهش میدهد و زمان زیادی از شما میگیرد. تکنیک تمیزکاری کدهای تست با Fixtures دقیقاً برای حل همین چالش ایجاد شده است تا فرآیند مدیریت وابستگیها را یک بار برای همیشه استاندارد کند. در بخش بعدی میبینیم که چطور این مشکل پنهان را ریشهکن میکنیم.
فیکسچرهای pytest دقیقاً چه مشکلاتی را حل میکنند؟
اصل حرف این است: وظیفه اصلی یک تابع تست، ارزیابی منطق برنامه است، نه اینکه بخش زیادی از خطوطش را صرف ساختن ابزارها و آمادهسازی محیط کند. وقتی فرآیند راهاندازی را درون خود توابع تست کپی میکنید، اصول تفکیک وظایف (Separation of Concerns) را زیر پا میگذارید. فیکسچرهای pytest دقیقاً برای حل همین بحران طراحی شدهاند.
مکانیزم کارکرد این ابزار هوشمند بر پایه تزریق خودکار نمونههای آماده است. به جای اینکه هر تابع تست به صورت مستقل اقدام به ساخت کلاس کیف پول دیجیتال کند، فیکسچر این مسئولیت را به عهده میگیرد. این ابزار به عنوان یک منبع تامینکننده وابستگی در پشت صحنه پروژه قرار میگیرد و محیط ارزیابی نرمافزار را کاملاً یکپارچه میکند.
با سپردن کدهای آمادهسازی به این قابلیت، شما چند مشکل بزرگ را به صورت همزمان ریشهکن میکنید:
حذف کدهای کثیف: کدهای راهاندازی مکرر از درون توابع حذف میشوند و بدنه تستها به شدت خلاصه و خوانا میشود.
نگهداری آسان و متمرکز: اگر متد سازنده کلاس اصلی تغییر کند، شما فقط یک خط کد را در خود فیکسچر اصلاح میکنید و تمام تستها بدون عیبونقص به کار خود ادامه میدهند.
ایزولهسازی کامل محیط: این ابزار تضمین میکند که هر تابع تست، یک نمونهسازی نو و دستنخورده تحویل بگیرد تا دادههای یک تست روی فرآیند عیبیابی کدهای مالی در سناریوی بعدی اثر منفی نگذارد.
این رویکرد مدرن در تمیزکاری کدهای تست با Fixtures، ساختار کدهای پایتون شما را از یک وضعیت شکننده و آماتور، به سطح استانداردهای معماری نرمافزار در سال ۲۰۲۶ میرساند. در بخش بعدی دست به کد میشویم تا اولین نمونه عملی آن را بسازیم.
ساخت اولین فیکسچر با دکوراتور pytest.fixture
برای ساختن اولین فیکسچر پایتون، باید از یک دکوراتور مخصوص استفاده کنیم که به فریمورک میفهماند این تابع قرار نیست خودش یک تست باشد، بلکه یک تامینکننده وابستگی برای بقیه تستهاست. فایل test_wallet.py را باز کنید. ابتدا ماژول اصلی را در خط اول ایمپورت میکنیم و سپس یک تابع ساده برای ساخت آبجکت کیف پول دیجیتال مینویسیم.
کدهای زیر را به ابتدای فایل تست خود اضافه کنید:
import pytest
from wallet import DigitalWallet
@pytest.fixture
def empty_wallet():
return DigitalWallet(owner="سهراب", balance=0.0)
@pytest.fixture
def wallet_with_funds():
return DigitalWallet(owner="آرش", balance=100.0)
بیایید کد بالا را بررسی کنیم. ما دو فیکسچرهای pytest کاملاً مجزا تعریف کردهایم. بالای سر هر تابع، دکوراتور @pytest.fixture قرار گرفته است. این نشانه به مکانیزم فرآیند شناسایی تستها اعلام میکند که این توابع، کدهای آمادهسازی محیط ارزیابی نرمافزار هستند.
تابع اول به نام empty_wallet یک نمونهسازی نو از کلاس اصلی با موجودی صفر انجام میدهد و آن را ریترن میکند. تابع دوم یعنی wallet_with_funds یک حساب شارژ شده با دارایی اولیه ۱۰۰ واحد خروجی میدهد.
مزیت این تفکیک کدهای راهاندازی این است که حالا برای سناریوهای مختلف مالی، ابزارهای آماده و تمیزی در پوشه تست داریم. نام این توابع دقیقاً به عنوان شناسه دسترسی آنها عمل خواهد کرد. در بخش بعدی میبینیم که چطور این نمونههای آماده را با تکنیک تزریق خودکار نمونههای آماده وارد بدنه تستها میکنیم تا تمیزکاری کدهای تست با Fixtures به طور کامل اجرا شود.
تزریق وابستگی (Dependency Injection) و استفاده از فیکسچر در توابع تست
حالا که فیکسچرهای pytest را تعریف کردیم، باید ببینیم چطور این نمونههای آماده را وارد بدنه توابع خود کنیم. جادوی فریمورک اینجاست که نیازی به فراخوانی دستی این توابع نیست. شما فقط کافی است نام فیکسچر را دقیقاً به عنوان یک آرگومان ورودی به تابع تست خود پاس بدهید.
فایل test_wallet.py را باز کنید و توابع زیر را به آن اضافه کنید تا مکانیزم تزریق خودکار نمونههای آماده را در عمل ببینید:
def test_deposit_on_empty_wallet(empty_wallet):
# ۱. اجرای متد روی فیکسچر تزریقشده
empty_wallet.deposit(50.0)
# ۲. صحتسنجی نهایی مانده حساب
assert empty_wallet.balance == 50.0
def test_withdraw_from_funded_wallet(wallet_with_funds):
# ۱. اجرای متد کسر موجودی
wallet_with_funds.withdraw(30.0)
# ۲. بررسی دارایی باقیمانده
assert wallet_with_funds.balance == 70.0
به ورودی توابع نگاه کنید. در تابع اول، عبارت empty_wallet را داخل پرانتز گذاشتهایم. وقتی ابزار ارزیابی نرمافزار این نام را میبیند، پوشه تست را میگردد تا فیکسچری با همین نام پیدا کند. سپس کدهای آمادهسازی آن را اجرا کرده و آبجکت خروجی را مستقیماً به داخل بدنه تست پرتاب میکند. این رویکرد مدرن، تزریق وابستگی یا Dependency Injection نام دارد.
داخل بدنه این تستها دیگر هیچ اثری از تکرار فرآیند راهاندازی یا کدهای کثیف نمونهسازی نیست. ما مستقیماً روی آبجکتهای آماده، متدهای مالی خود را صدا میزنیم و وضعیت آنها را ارزیابی میکنیم.
این شیوه از تمیزکاری کدهای تست با Fixtures به شما اجازه میدهد تمرکز خود را کاملاً روی منطق بیزینس و سناریوهای مالی بگذارید. در بخش بعدی بررسی میکنیم که پایتون چطور امنیت این آبجکتها را تامین میکند تا دادههای تستها با هم تداخل پیدا نکنند.
ایزولهسازی محیط ارزیابی نرمافزار و چرخه حیات پیشفرض فیکسچرها
یک سوال حیاتی که برای بیشتر برنامه نویس ها پیش میآید این است: اگر یکی از توابع، موجودی آبجکت تزریقشده را تغییر دهد، آیا دارایی اولیه کیف پول در سناریوی بعدی هم خراب میشود؟ پاسخ کوتاه، خیر است. فریمورک با رعایت اصل ایزولهسازی محیط ارزیابی نرمافزار جلوی این تداخلها را میگیرد.
دلیل این امنیت بالا، به چرخه حیات پیشفرض فیکسچرها برمیگردد. وقتی دکوراتور را بدون هیچ تنظیمات اضافهای به کار میبرید، اسکوپ یا محدوده اجرای آن روی حالت تابع (Function Scope) تنظیم میشود. این یعنی ابزار تست به تعداد توابعی که نام فیکسچر را صدا زدهاند، نمونهسازی نو و کاملاً مستقل انجام میدهد.
بیایید روند کار را در پشت صحنه پروژه دنبال کنیم:
ابتدا تستِ اول، آبجکت wallet_with_funds را با ۱۰۰ واحد دارایی اولیه تحویل میگیرد و ۳۰ واحد از آن کسر میکند. مانده حساب این آبجکت حالا ۷۰ واحد است.
به محض اتمام تست اول، این آبجکت به طور کامل از حافظه پاک میشود.
حالا تستِ دوم، همان فیکسچر را صدا میزند. فریمورک دوباره کدهای آمادهسازی را از اول اجرا میکند و یک آبجکت کاملاً جدید با دارایی اولیه ۱۰۰ واحد به تابع دوم تحویل میدهد.
این تفکیک دقیق مانع از بروز خطاهای زنجیرهای میشود. اگر دیتای یک تست روی سناریوی بعدی اثر بگذارد، با وضعیت خطرناکی به نام تستهای کثیف یا وابسته روبرو میشوید که پیدا کردن ریشه باگها را غیرممکن میکند. با تمیزکاری کدهای تست با Fixtures و درک درست از این چرخه حیات، مطمئن خواهید بود که هر سناریو در یک محیط آزمایشگاهی ایزوله و بدون نویز اجرا میشود. حالا ترمینال را باز کنید و دستور pytest را بزنید تا نوار سبز رنگ موفقیت را برای این کدهای بهینهشده ببینید.
چطور یک فیکسچر را مجبور کنیم بدون پاس دادن به آرگومان ورودی، خودکار اجرا شود؟ (قابلیت autouse)
گاهی اوقات در فرآیند مدیریت وابستگیها با فیکسچرهایی سر و کار دارید که هیچ دیتایی را ریترن نمیکنند، اما حضورشان برای آمادهسازی محیط ارزیابی نرمافزار حیاتی است؛ مثلاً فیکسچری که قبل از هر سناریو، جدولهای دیتابیس تستی را پاکسازی میکند یا زمان شروع تست را در فایل لاگ پروژه ثبت میآورد.
اگر بخواهید نام این فیکسچرهای جانبی را به عنوان آرگومان ورودی به تکتک توابع تست خود پاس دهید، دوباره دچار چالش کدهای تکراری میشوید. فریمورک برای حل این موضوع، یک فلگ هوشمند به نام autouse را در دکوراتور خود قرار داده است.
فایل test_wallet.py را باز کنید و این نمونه کد را برای درک بهتر قابلیت تزریق خودکار نمونههای آماده اضافه کنید:
@pytest.fixture(autouse=True)
def log_test_trigger():
print("\n--- یک سناریوی مالی جدید شروع شد ---")
با اضافه کردن عبارت autouse=True به ورودی دکوراتور، این تابع تبدیل به یک فیکسچر خودکار میشود. حالا بدون اینکه نام این تابع را در پرانتز توابع تست خود بنویسید، مکانیزم فرآیند شناسایی تستها به صورت هوشمند آن را پیدا میکند. پایتون این کد را مثل یک سایه، دقیقاً قبل از شروع چرخه حیات پیشفرض فیکسچرها در هر تست اجرا میکند.
این ویژگی کلید اصلی تمیزکاری کدهای تست با Fixtures در سطوح پیشرفته است. با این ترفند، بدون شلوغ کردن ورودی توابع، زیرساخت و کدهای آمادهسازی پوشه تست شما همیشه در پشت صحنه به صورت خودکار و منظم مدیریت میشوند. حالا تمام فایلها را ذخیره کنید تا در درس بعدی سراغ تکنیکهای پیشرفتهتر فیکسچرها برویم.