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

بسیاری از برنامه‌نویسان فکر می‌کنند ابزارهای ارزیابی نرم‌افزار پکیج‌های پیچیده‌ای دارند، اما تمام جادوی فریم‌ورک pytest روی یک کلمه کلیدی ساده و بومی در پایتون به نام assert می‌چرخد.

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

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

پیاده‌سازی منطق افزایش موجودی در کلاس کیف پول

برای شروع، باید کدهای اصلی برنامه را درون فایل wallet.py بنویسیم. در این مرحله، یک کلاس پایتونی به نام DigitalWallet خلق می‌کنیم که قرار است نقش هسته مرکزی سیستم مالی ما را بازی کند. این کلاس در زمان ساخته شدن، نام صاحب حساب و موجودی اولیه را دریافت می‌کند.

فایل wallet.py را در ادیتور خود باز کنید و این ساختار اولیه را درون آن پیاده‌سازی کنید:

class DigitalWallet:
    def __init__(self, owner: str, balance: float = 0.0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount: float):
        if amount <= 0:
            raise ValueError("مبلغ واریز باید بیشتر از صفر باشد.")
        self.balance += amount

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

بخش اصلی کار ما، متد deposit است. این تابع وظیفه مدیریت عملیات واریز و افزایش موجودی را بر عهده دارد. قبل از اینکه هرگونه عملیات ریاضی روی حساب انجام شود، یک شرط امنیتی گذاشته‌ایم تا مطمئن شویم مبلغ ورودی بزرگ‌تر از صفر است. اگر کسی تلاش کند عدد منفی یا صفر را وارد کند، برنامه یک خطای لایه اپلیکیشن از نوع ValueError پرتاب می‌کند و جلوی ادامه کار را می‌گیرد. در غیر این صورت، مبلغ مستقیماً به متغیر self.balance اضافه می‌شود.

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

کالبدشکافی دستور assert و جادوی آن در pytest

دستور assert در پایتون یک ابزار بومی و داخلی است که برای ادعا کردن به کار می‌رود. وقتی از این کلمه کلیدی استفاده می‌کنید، در واقع یک شرط ریاضی یا منطقی را جلوی آن می‌نویسید و به پایتون می‌گویید: «من ادعا می‌کنم که نتیجه این عبارت حتماً درست است».

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

ساختار کلی نوشتن یک ادعا در این ساختار به این شکل است:

assert عبارت_مورد_نظر == مقدار_مورد_انتظار

جادوی اصلی زمانی اتفاق می‌افتد که شما از این دستور ساده در فریم‌ورک pytest استفاده می‌کنید. در ابزارهای قدیمی‌تر مثل unittest، شما مجبور بودید برای هر نوع مقایسه، متدهای عجیب و غریبی مثل assertEqual یا assertTrue را حفظ کنید. اما این فریم‌ورک تمام این پیچیدگی‌ها را حذف کرده است.

ابزار pytest کدهای شما را قبل از اجرا بازنویسی می‌کند (Assert Rewriting). با این ترفند فنی، وقتی یک ادعا شکست می‌خورد، فریم‌ورک فقط به گفتن کلمه «غلط است» بسنده نمی‌کند؛ بلکه تمام پشت‌صحنه متغیرها، مقداری که تابع تولید کرده و مقداری که شما انتظار داشتید را با نمودارهای متنی در خط فرمان به تصویر می‌کشد.

یادگیری کار با این ابزار، کلید اصلی موفقیت در ارزیابی نرم‌افزار است. شما به کمک عملگرهای ساده‌ای مثل == برای تساوی، != برای عدم تساوی، یا حتی کلمه in برای بررسی وجود یک عضو در لیست، می‌توانید پیچیده‌ترین رفتارهای کدهای نرم‌افزار را در کسری از ثانیه بسنجید. در بخش بعدی، این جادو را روی متد افزایش موجودی پیاده می‌کنیم.

نوشتن اولین سناریوی تست برای متد واریز وجه

حالا وقت آن است که منطق کلاس کیف پول را به بوته آزمایش بگذاریم. برای این کار، سراغ پوشه تست می‌رویم و فایل tests/test_wallet.py را باز می‌کنیم. در این فایل، یک سناریوی واقعی را شبیه‌سازی می‌کنیم: ابتدا یک حساب کاربری می‌سازیم، مقداری پول به آن واریز می‌کنیم و در نهایت با نوشتن اولین تست پایتون با دستور assert، بررسی می‌کنیم که آیا محاسبات ریاضی سیستم درست کار می‌کند یا خیر.

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

from wallet import DigitalWallet

def test_wallet_deposit_increases_balance():
    # ۱. آماده‌سازی محیط تست (Setup)
    wallet = DigitalWallet(owner="محمد", balance=50.0)
    
    # ۲. اجرای تابع اصلی (Action)
    wallet.deposit(30.0)
    
    # ۳. ارزیابی نتیجه (Assert)
    assert wallet.balance == 80.0

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

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

داخل تابع، کار را به سه بخش کلیدی تقسیم کردیم:
ابتدا یک نمونه از کیف پول با دارایی اولیه ۵۰ تومان ساختیم. در گام دوم، متد deposit را با مبلغ ۳۰ تومان صدا زدیم. در خط پایانی، به کمک عملگر تساوی ریاضی در دستور assert ادعا کردیم که خروجی نهایی متغیر موجودی، باید دقیقاً عدد ۸۰ باشد. فایل را ذخیره کنید تا در بخش بعدی خروجی این ادعا را در خط فرمان ببینیم.

اجرای تست و تحلیل گزارش موفقیت در محیط خط فرمان

کدها آماده هستند و حالا باید سناریویی که نوشتیم را اجرا کنیم. ترمینال را باز کنید و مطمئن شوید که در پوشه اصلی پروژه (همان دایرکتوری که فایل wallet.py در آن قرار دارد) هستید. محیط مجازی را فعال نگه دارید و این دستور ساده را تایپ کنید:

pytest

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

============================== test session starts ==============================
platform linux -- Python 3.11.5, pytest-8.3.2
rootdir: /home/user/digital_wallet_project
collected 1 item

tests/test_wallet.py .                                                    [100%]

============================== 1 passed in 0.02s ==============================

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

جلوی مسیر فایل tests/test_wallet.py یک علامت نقطه سبزرنگ (.) ثبت شده است. این تک نقطه در ادبیات pytest یعنی یک تابع تست با موفقیت پاس شده است. اگر ده تست موفق داشتید، ده نقطه سبز پشت سر هم ردیف می‌شد.

در نهایت، نوار سبز رنگ پایانی با عبارت 1 passed به شما اعلام می‌کند که ادعای مطرح‌شده در دستور assert کاملاً با رفتار کدهای اصلی برنامه همخوانی دارد. موجودی اولیه ۵۰ تومانی بعد از واریز ۳۰ تومان دقیقاً به رقم ۸۰ رسیده و هیچ تناقضی در منطق محاسباتی سیستم وجود ندارد. این نوار سبز، اولین تاییدیه رسمی برای سلامت کیف پول دیجیتال شماست.

به چالش کشیدن تست با تزریق عمده باگ محاسباتی

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

سراغ فایل کدهای اصلی یعنی wallet.py بروید و متد واریز وجه را به این صورت دستکاری کنید:

def deposit(self, amount: float):
        if amount <= 0:
            raise ValueError("مبلغ واریز باید بیشتر از صفر باشد.")
        self.balance -= amount  # باگ عمدی: تبدیل جمع به تفریق

علامت پلاس را به ماینس تبدیل کردیم. با این کار، عملیات افزایش موجودی عملاً تبدیل به برداشت از حساب می‌شود. حالا ترمینال را باز کنید و دوباره دستور pytest را اجرا کنید.

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

=================================== FAILURES ===================================
_____________________ test_wallet_deposit_increases_balance ____________________

    def test_wallet_deposit_increases_balance():
        wallet = DigitalWallet(owner="محمد", balance=50.0)
        wallet.deposit(30.0)
>       assert wallet.balance == 80.0
E       assert 20.0 == 80.0
E        +  where 20.0 = <wallet.DigitalWallet object>.balance

tests/test_wallet.py:11: AssertionError
=========================== short test summary info ============================
FAILED tests/test_wallet.py::test_wallet_deposit_increases_balance - assert 20.0 == 80.0
============================== 1 failed in 0.05s ==============================

بیایید این گزارش شکست (Failed) را کالبدشکافی کنیم. فریم‌ورک با علامت > دقیقاً خطی که باعث سقوط تست شده را فاش می‌کند: assert wallet.balance == 80.0.

کمی پایین‌تر، در خطوطی که با پیشوند E (مخفف Error) شروع می‌شوند، دلیل صادر شدن خطای AssertionError با جزئیات ریاضی ثبت شده است. ابزار به شما می‌گوید: assert 20.0 == 80.0. یعنی سیستم متوجه شده که موجودی فعلی کیف پول دیجیتال به جای ۸۰، عدد ۲۰ است. ۵۰ تومان موجودی اولیه منهای ۳۰ تومان واریزی، شده ۲۰ تومان!

این گزارش خط فرمان به وضوح جادوی کار با دستور assert را نشان می‌دهد. بدون اینکه نیاز باشد برنامه را به صورت دستی روی سرور یا با متدهای print تست کنید، فریم‌ورک مچ باگ محاسباتی را در کسری از ثانیه گرفت. حالا که از هوشیاری تست خود مطمئن شدید، علامت منفی را در فایل wallet.py دوباره به مثبت تبدیل کنید تا پروژه به وضعیت سلامت کامل برگردد.

چالش کوچک: متد برداشت از حساب (Withdraw) رو تو بنویس!

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

برای شروع این چالش، گام‌های زیر را قدم‌به‌قدم در پروژه خود پیاده‌سازی کنید:

گام اول: توسعه منطق برداشت در فایل کدهای اصلی

فایل wallet.py را باز کنید و یک متد جدید به نام withdraw درون کلاس تعریف کنید. این تابع باید مبلغ برداشت را دریافت کرده و از متغیر موجودی کسر کند. حواستان به لایه امنیتی نرم‌افزار باشد؛ نباید اجازه دهید کاربر بیشتر از موجودی فعلی حسابش، پول برداشت کند. اگر چنین اتفاقی افتاد، باید یک خطای ValueError پرتاب شود.

گام دوم: نوشتن سناریوی تست کسر موجودی

سراغ پوشه تست بروید و در فایل test_wallet.py یک تابع تست جدید با پیشوند استاندارد بسازید؛ مثلاً نام آن را test_wallet_withdraw_decreases_balance بگذارید تا فرآیند شناسایی تست‌ها (Test Discovery) بدون مشکل انجام شود. درون این تابع، یک کیف پول با موجودی اولیه ۱۰۰ تومان بسازید، مبلغ ۳۰ تومان را برداشت کنید و در نهایت با یک دستور assert ادعا کنید که مانده حساب باید دقیقاً عدد ۷۰ باشد.

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