در درس های قبلی، با یک سوال ساده شروع کردیم: چطور مطمئن شویم کدی که نوشتیم درست کار می‌کند؟
آن سوال، مسیر کاملی را باز کرد.
از تفاوت تست دستی و خودکار گذشتیم. یاد گرفتیم چطور با pytest اولین تست را بنویسیم، چطور با parametrize ده‌ها سناریو را با چند خط پوشش دهیم، و چطور با فیکسچر، کد تکراری را از تست‌ها بیرون بکشیم. دیدیم که conftest.py چطور فیکسچرها را بین فایل‌های مختلف به اشتراک می‌گذارد، با مارکرها تست‌ها را دسته‌بندی کردیم، با Mocking دنیای بیرون را شبیه‌سازی کردیم، پوشش کد را با pytest-cov اندازه گرفتیم، و یاد گرفتیم توابعی که با فایل، JSON و دیتابیس کار می‌کنند را هم ایزوله تست کنیم. این مجموعه مهارت‌ها، پایه‌ای است که اکثر توسعه‌دهندگان پایتون در کار روزمره به آن نیاز دارند.

در این درس چه چیزی برای ارائه داریم؟

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

مرور سفر طی‌شده در دوره

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

از ایده تا اولین تست

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

ابزارهایی که کد تست را حرفه‌ای‌تر کردند

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

وقتی کد با دنیای بیرون ارتباط داشت

مارکرها اجازه دادند تست‌ها را دسته‌بندی کنید و فقط بخشی از آن‌ها را اجرا کنید. این مهارت در پروژه‌های بزرگ، زمان اجرای تست را از دقیقه‌ها به ثانیه‌ها کاهش می‌دهد. Mocking یکی از مهم‌ترین مهارت‌های این دوره بود. توابعی که به سرویس‌های خارجی وابسته بودند، با شبیه‌سازی رفتارشان تست شدند. قانون طلایی «ماک کنید جایی که استفاده می‌شود» یک اصل فنی مهم است که در هر پروژه واقعی به کار می‌آید.
pytest-cov نشان داد کدام بخش‌های برنامه هنوز تست ندارند. Coverage به تنهایی کیفیت تست را تضمین نمی‌کند، اما نقاط کور را آشکار می‌کند. در آخرین مهارت فنی دوره، توابعی که با فایل، JSON و دیتابیس کار می‌کردند تست شدند. tmp_path و دیتابیس حافظه‌ای SQLite این امکان را دادند که محیط تست کاملاً از داده‌های واقعی پروژه جدا بماند.

تصویر کلی

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

اجرای تست‌ها در CI/CD با GitHub Actions

تست‌نویسی روی کامپیوتر شخصی یک مشکل پنهان دارد: فقط روی همان کامپیوتر اجرا می‌شود.
یک توسعه‌دهنده کد را تغییر می‌دهد، تست‌ها را روی سیستم خودش اجرا می‌کند، سبز می‌شوند و کد را آپلود می‌کند. اما روی سرور یا کامپیوتر همکار، همان تست‌ها شکست می‌خورند؛ چون یک کتابخانه نصب نشده یا نسخه پایتون متفاوت است. CI/CD دقیقاً برای حل این مشکل ساخته شده.

CI/CD چیست؟

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

GitHub Actions چطور کار می‌کند؟

GitHub Actions یک ابزار اتوماسیون قدرتمند و انعطاف‌پذیر است که مستقیم داخل پلتفرم GitHub جا گرفته. امکان ساخت workflow های سفارشی را می‌دهد تا وظایف مختلفی مثل ساخت، تست و استقرار نرم‌افزار به‌صورت خودکار انجام شوند.  تمام تنظیمات GitHub Actions در یک فایل YAML ذخیره می‌شود. YAML یک فرمت ساده برای نوشتن تنظیمات است که با فاصله‌گذاری (indentation) ساختار پیدا می‌کند.

ساختار پوشه‌ها

برای اینکه GitHub Actions تست‌های پروژه کیف پول دیجیتال را بشناسد، باید یک پوشه و فایل مشخص ساخته شود:

digital_wallet_project/
├── .github/
│   └── workflows/
│       └── pytest.yml
├── wallet.py
└── tests/
    └── test_wallet.py

پوشه .github/workflows/ جایی است که GitHub به‌صورت خودکار دنبالش می‌گردد. هر فایل YAML داخل آن، یک workflow مستقل است.

فایل پیکربندی برای پروژه کیف پول دیجیتال

محتوای فایل pytest.yml برای این پروژه:

name: Run Pytest

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pytest

      - name: Run tests
        run: pytest

هر بخش این فایل یک وظیفه مشخص دارد.
on مشخص می‌کند این workflow چه زمانی اجرا شود. تنظیم push و pull_request روی شاخه main یعنی هر بار که کدی به این شاخه آپلود شود یا یک Pull Request باز شود، workflow به‌صورت خودکار شروع می‌شود. 
runs-on: ubuntu-latest یعنی تست‌ها روی یک سرور لینوکسی اجرا می‌شوند. این سرور هر بار از صفر شروع می‌کند؛ هیچ چیزی از اجرای قبلی باقی نمی‌ماند. چهار مرحله steps هم ترتیب منطقی دارند: اول کد دریافت می‌شود، بعد پایتون نصب می‌شود، بعد وابستگی‌ها نصب می‌شوند، و در آخر pytest اجرا می‌شود.

نتیجه را کجا ببینیم؟

بعد از اینکه فایل workflow به مخزن اضافه و commit شد، در GitHub به بخش Actions بروید تا نتیجه اجرا را ببینید. یک علامت سبز یعنی همه تست‌ها پاس شدند. یک علامت قرمز یعنی حداقل یک تست شکست خورده و باید بررسی شود.  GitHub Actions برای مخازن عمومی رایگان است و برای مخازن خصوصی ۲۰۰۰ دقیقه در ماه رایگان ارائه می‌دهد. برای یک پروژه مقدماتی مثل کیف پول دیجیتال، این سقف عملاً نامحدود است.

یک نکته مهم

فایل YAML به فاصله‌گذاری بسیار حساس است. اگر یک خط اشتباه تورفتگی داشته باشد، workflow اجرا نمی‌شود. بهترین روش برای جلوگیری از این مشکل، استفاده از یک ادیتور مثل VS Code است که خطاهای YAML را قبل از آپلود نشان می‌دهد. با این تنظیم ساده، پروژه کیف پول دیجیتال از یک پروژه محلی به یک پروژه حرفه‌ای تبدیل می‌شود که هر تغییر در آن، به‌صورت خودکار تأیید می‌شود.

معرفی ابزارهای مکمل pytest

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

pytest-mock

در درس Mocking، از unittest.mock استفاده کردیم. این کتابخانه داخلی پایتون است و نیازی به نصب جداگانه ندارد. pytest-mock یک لایه نازک روی همین کتابخانه است که کار با Mock را در محیط pytest راحت‌تر می‌کند. تفاوت اصلی در نحوه استفاده است. به جای دکوراتور @patch، یک فیکسچر به نام mocker مستقیم در تابع تست استفاده می‌شود:

def test_send_notification(mocker):
    mock_email = mocker.patch("wallet.send_email")
    notify_user("علی", 100.0)
    mock_email.assert_called_once()

این ابزار برای Mock کردن اشیاء بیرونی، توابع، کلاس‌ها، فراخوانی‌های API و منابع ابری بسیار مفید است. 
نصب آن با یک دستور ساده انجام می‌شود:

pip install pytest-mock

pytest-xdist

وقتی مجموعه تست‌ها بزرگ می‌شود، اجرای همه آن‌ها به صورت متوالی وقت می‌برد. pytest-xdist pytest را با حالت‌های جدید اجرا گسترش می‌دهد؛ پرکاربردترین آن‌ها توزیع تست‌ها بین چند CPU برای سرعت بخشیدن به اجراست. 
با یک پرچم ساده، تست‌ها به صورت موازی اجرا می‌شوند:

pytest -n auto

با این دستور، pytest به تعداد CPUهای موجود، فرآیند کارگر (worker) می‌سازد و تست‌ها را به صورت تصادفی بین آن‌ها توزیع می‌کند.  برای پروژه کیف پول دیجیتال با تعداد محدود تست، این ابزار تفاوت محسوسی ایجاد نمی‌کند. اما در پروژه‌هایی با صدها تست، زمان اجرا می‌تواند به یک‌سوم کاهش پیدا کند.

Faker

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

from faker import Faker

fake = Faker()

def test_wallet_with_random_owner():
    owner_name = fake.name()
    wallet = DigitalWallet(owner=owner_name, balance=100.0)
    assert wallet.owner == owner_name

هر بار که این تست اجرا می‌شود، یک نام متفاوت آزمایش می‌شود. این کار سناریوهای غیرمنتظره را بهتر پوشش می‌دهد.

pytest-timeout

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

pytest --timeout=10

این دستور می‌گوید هر تستی که بیش از ۱۰ ثانیه طول بکشد، شکست خورده تلقی شود.

کدام ابزار را اول یاد بگیرید؟

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

تست‌نویسی در پروژه‌های واقعی

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

چه زمانی تست بنویسیم؟

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

Coverage چقدر باشد؟

این سوالی است که در هر تیم به نتیجه متفاوتی می‌رسد. برای اکثر پروژه‌ها، هدف‌گذاری بین ۸۰ تا ۹۰ درصد یک هدف واقع‌بینانه است. روی مسیرهای حیاتی تمرکز کنید؛ اطمینان حاصل کنید که کدهای مهم کسب‌وکار پوشش کامل دارند. 
رسیدن به ۱۰۰٪ Coverage در دنیای واقعی اغلب ممکن نیست. برنامه‌های واقعی پیچیده هستند، در چندین فایل و ماژول تقسیم شده‌اند و با پایگاه‌های داده و سرویس‌های خارجی در ارتباطند. 
عدد Coverage به تنهایی کافی نیست. یک تست بد که فقط خط‌ها را اجرا می‌کند ولی هیچ چیز را بررسی نمی‌کند، Coverage را بالا می‌برد اما باگ را پیدا نمی‌کند. هدف نهایی Coverage بالا نیست؛ بلکه ابزاری است برای شناسایی بخش‌های تست‌نشده. 

تست‌های شکننده؛ مشکل رایج پروژه‌های بزرگ

تست شکننده (Flaky Test) تستی است که گاهی سبز می‌شود و گاهی قرمز، بدون اینکه کدی تغییر کرده باشد. این مشکل در پروژه‌های بزرگ بسیار رایج است. دلایل معمول: وابستگی به زمان سیستم، وابستگی به ترتیب اجرای تست‌ها، یا وابستگی به داده‌ای که تست قبلی تغییر داده. راه‌حل پایه همان چیزی است که در این دوره یاد گرفتید: هر تست باید محیط خودش را بسازد و بعد از تمام شدن، پاک کند. اگر با تست شکننده روبرو شدید، آن را موقتاً با @pytest.mark.skip علامت‌گذاری کنید تا pipeline قرمز نشود، بعد ریشه مشکل را پیدا کنید.

اولویت‌بندی در شرایط کمبود وقت

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

تست‌نویسی یک عادت است، نه یک وظیفه

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

نقشه راه یادگیری بعد از این دوره

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

مرحله اول: TDD را عمیق‌تر یاد بگیرید

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

مرحله دوم: تست یکپارچگی را یاد بگیرید

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

مرحله سوم: تست API با FastAPI یا Django

اکثر برنامه‌های پایتونی امروزی یک API دارند. FastAPI یک ابزار داخلی به نام TestClient دارد که امکان استفاده مستقیم از pytest را فراهم می‌کند. یعنی همان مهارت‌هایی که در این دوره یاد گرفتید، مستقیم در تست API هم به کار می‌روند. Pytest with Eric
یک تست ساده برای یک endpoint در FastAPI به این شکل است:

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_get_wallet_balance():
    response = client.get("/wallet/balance")
    assert response.status_code == 200
    assert "balance" in response.json()

همین رویکرد برای Django هم وجود دارد؛ پیکربندی pytest برای Django و نوشتن تست‌های واحد و یکپارچگی در کنار هم. 

مرحله چهارم: Property-Based Testing با Hypothesis

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

مرحله پنجم: تست در پروژه‌های متن‌باز

خواندن کد تست پروژه‌های واقعی یکی از بهترین روش‌های یادگیری است. روی GitHub پروژه‌هایی مثل httpx، pydantic یا fastapi را پیدا کنید و پوشه tests/ آن‌ها را مطالعه کنید. خواهید دید که همان ابزارهایی که در این دوره یاد گرفتید، فیکسچر، conftest، parametrize و mocking، در پروژه‌های بزرگ و واقعی هم دقیقاً به همان شکل استفاده می‌شوند.

از کجا شروع کنید؟

اگر قرار باشد فقط یک قدم بردارید، پیشنهاد این است که همین پروژه کیف پول دیجیتال را گسترش دهید. یک endpoint ساده با Django REST Framework به آن اضافه کنید و تست آن را با APIClient بنویسید. این یک قدم، مسیر بعدی را کاملاً روشن می‌کند.