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

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

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

چالش‌های تست دستی در پروژه‌های واقعی

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

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

۱. هدر رفتن شدید زمان و انرژی

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

۲. خطای دید و خستگی مفرط ذهن

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

۳. ترس از دست بردن در کدهای قدیمی (Regression)

بزرگ‌ترین آسیب تست دستی، ایجاد ترس در تیم فنی است. وقتی شما ساختار یک کد قدیمی را بازنویسی (Refactor) می‌کنید، هرگز مطمئن نیستید که کدام بخش‌های دیگر سیستم را ناخواسته خراب کرده‌اید. از آنجا که تست دستی همه بخش‌ها روزها زمان می‌برد، ترجیح می‌دهید به کدهای قدیمی و ناکارآمد دست نزنید؛ اتفاقی که کیفیت فنی پروژه را به مرور زمان نابود می‌کند.

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

مفهوم تست خودکار (Automated Testing) و مزایای آن

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

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

استفاده از این روش، سه مزیت کلیدی برای کیفیت فنی پروژه و آینده شغلی شما دارد:

۱. بازخورد سریع در زمان توسعه (Fast Feedback)

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

۲. مستندسازی زنده و واقعی کد

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

۳. رهایی از استرس دپلوی (Deployment)

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

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

تشریح سناریوی پروژه: سیستم کیف پول دیجیتال (Digital Wallet)

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

پروژه ما یک کلاس پایتونی به نام DigitalWallet خواهد بود که ساختار داده‌ها و رفتارهای مالی کاربران را مدیریت می‌کند.

این سیستم برای کارکرد درست، باید ۴ نیازمندی و قانون اصلی را پوشش دهد:

۱. مدیریت و تفکیک حساب کاربران

سیستم باید بتواند برای هر کاربر یک کیف پول مجزا با یک شناسه منحصربه‌فرد (UUID یا نام کاربری) ایجاد کند. هر کیف پول در لحظه ساخت، یک موجودی اولیه دارد که این مقدار نمی‌تواند منفی باشد.

۲. عملیات واریز وجه (Deposit)

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

۳. عملیات برداشت وجه (Withdraw)

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

۴. انتقال وجه بین کاربران (Transfer)

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

انتخاب این سناریو به این دلیل است که منطق برنامه کاملاً بر اساس شرط‌ها و محاسبات عددی پیش می‌رود؛ یعنی دقیقاً همان نقاطی که بیشترین پتانسیل را برای تولید باگ دارند و بهترین بستر برای نوشتن تست‌های جامع با pytest هستند.

پیاده‌سازی منطق اولیه کیف پول با پایتون خالص

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

یک فایل جدید به نام wallet.py ایجاد کنید و کدهای زیر را درون آن بنویسید:

class InsufficientFundsError(Exception):
    pass

class DigitalWallet:
    def __init__(self, owner: str, initial_balance: float = 0.0):
        if initial_balance < 0:
            raise ValueError("موجودی اولیه نمی‌تواند منفی باشد.")
        self.owner = owner
        self.balance = initial_balance

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

    def withdraw(self, amount: float):
        if amount <= 0:
            raise ValueError("مبلغ برداشت باید بیشتر از صفر باشد.")
        if amount > self.balance:
            raise InsufficientFundsError("موجودی حساب شما کافی نیست.")
        self.balance -= amount
        return self.balance

بیا منطق این کد را خیلی سریع بررسی کنیم تا ببینیم چه اتفاقی افتاده است:

ساختار کلاس و مدیریت خطاها

در بالاترین بخش کد، یک خطای اختصاصی به نام InsufficientFundsError تعریف کرده‌ایم. این کار به ما کمک می‌کند تا زمان برداشت وجه غیرمجاز، خطایی دقیق و واضح صادر کنیم، نه یک خطای عمومی پایتون.

در متد سازنده (__init__)، نام صاحب کیف پول و موجودی اولیه را می‌گیریم. جلوی ساخت کیف پول با موجودی منفی را هم همان اول کار با یک شرط ساده گرفته‌ایم.

متدهای واریز و برداشت

متد deposit مسئول افزایش موجودی است. این متد ابتدا مطمئن می‌شود که عدد ورودی مثبت است، سپس آن را به موجودی قبلی اضافه می‌کند.

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

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

تزریق باگ فرضی و بررسی رفتار سیستم

کدهای ما در ظاهر بدون نقص کار می‌کنند. اما در دنیای واقعی، تغییرات مداوم روی کدها یا یک حواس‌پرتی ساده هنگام توسعه، می‌تواند فاجعه بسازد. برای درک بهتر این موضوع، بیایید خودمان عمداً یک باگ محاسباتی پنهان وارد سیستم کنیم.

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

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

def withdraw(self, amount: float):
        if amount <= 0:
            raise ValueError("مبلغ برداشت باید بیشتر از صفر باشد.")
        if amount > self.balance:
            raise InsufficientFundsError("موجونی حساب شما کافی نیست.")
        
        # باگ فرضی: موجودی کسر می‌شود اما در متغیر ذخیره نمی‌شود
        self.balance - amount 
        return self.balance

بیا ببینیم با این تغییر کوچک چه اتفاقی برای سیستم افتاد:

تحلیل رفتار سیستم پس از خرابکاری

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

عجیب‌تر این است که اگر برنامه را اجرا کنید، هیچ خطایی (Error) در ترمینال نمی‌بینید. از نظر مفسر پایتون، عبارت self.balance - amount یک دستور کاملاً قانونی است. برنامه اجرا می‌شود، پول فرضی را کم می‌کند و بدون هیچ مشکلی موجودی قبلی را بازمی‌گرداند!

خطرات این نوع باگ‌ها در سیستم‌های مالی

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

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

مقایسه عینی سرعت و دقت: تست دستی در برابر تست خودکار

حالا که باگ را درون کد کاشتیم، بیایید ببینیم پیدا کردن آن در دو دنیای مختلف چقدر تفاوت دارد.

روش اول: تست دستی و سنتی

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

مثلاً چنین کدی می‌نویسیم:

from wallet import DigitalWallet

wallet = DigitalWallet("محمد", 100.0)
wallet.withdraw(30.0)
print("موجودی پس از برداشت باید ۷۰ باشد. موجودی فعلی:", wallet.balance)

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

روش دوم: تست خودکار با pytest

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

from wallet import DigitalWallet

def test_wallet_withdraw():
    wallet = DigitalWallet("محمد", 100.0)
    wallet.withdraw(30.0)
    assert wallet.balance == 70.0

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

حالا کافی است ترمینال را باز کنیم و دستور pytest را بزنیم. در کمتر از یک ثانیه، فریم‌ورک تمام فایل‌های تست را اسکن می‌کند و با یک خروجی قرمز رنگ، مچ باگ را می‌گیرد. این ابزار دقیقاً به ما نشان می‌دهد که خروجی کد ۱۰۰ بوده، در حالی که ما انتظار عدد ۷۰ را داشتیم.

مقایسه نهایی سرعت و دقت

شاخص مقایسه تست دستی (Manual) تست خودکار (Automated)
سرعت اجرا کند و وابسته به سرعت عمل شما کمتر از یک ثانیه برای صدها تست
دقت بررسی احتمال خطای دید و خستگی چشم صفر (کامپیوتر محاسبات را بررسی می‌کند)
قابلیت تکرار هر بار باید مراحل را از اول انجام دهید با یک دستور ساده بارها تکرار می‌شود
ارزش شغلی مخصوص پروژه‌های کوچک و محلی استاندارد اجباری در شرکت‌های بزرگ

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