FA-TOOLS — Header Component

آموزش unittest در پایتون + نمونه کد تست واحد و سناریوهای کاربردی

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

تست واحد (Unit Test) چیست و چرا unittest پایتون؟

آموزش unittest در پایتون + نمونه کد تست واحد — تصویر 1

تست واحد (Unit Testing) به معنای سنجش صحت عملکرد کوچک‌ترین قطعات کد قابل تست در برنامه (مانند توابع یا متدهای یک کلاس) به صورت کاملاً مستقل و ایزوله است. کتابخانه unittest ماژول استاندارد پایتون برای پیاده‌سازی این تست‌ها بر پایه الگوریتم‌ها و ساختارهای شیءگرا است.

در پروژه‌های نرم‌افزاری واقعی، تغییر در یک بخش از کد می‌تواند منجر به بروز باگ‌های ناخواسته در بخش‌های دیگر (Regression) شود. استفاده از یک فریم‌ورک تست استاندارد مانع از انتشار این خطاها به محیط عملیاتی می‌شود. دلیل اصلی محبوبیت unittest این است که به‌صورت توکار (Built-in) همراه پایتون نصب می‌شود، نیازی به ابزارهای جانبی ندارد و با ابزارهای CI/CD کاملاً سازگار است.

مفاهیم پایه و ساختار اصلی کتابخانه unittest

آموزش unittest در پایتون + نمونه کد تست واحد — تصویر 2

فریم‌ورک unittest بر چهار مفهوم کلیدی استوار است که درک آن‌ها برای نگارش تست‌های حرفه‌ای ضروری است:

  • Test Case: کوچک‌ترین واحد تست که صحت یک پاسخ یا رفتار خاص را بررسی می‌کند. این بخش از کلاس unittest.TestCase ارث‌بری می‌کند.
  • Test Fixture: آماده‌سازی مقدمات لازم جهت اجرای یک یا چند تست (مانند ایجاد پایگاه‌داده موقت، باز کردن فایل یا ساخت اشیاء اولیه) و پاک‌سازی آن‌ها پس از اتمام کار.
  • Test Suite: مجموعه‌ای از چندین TestCase یا Test Suite دیگر که برای اجرای هم‌زمان گروه‌بندی شده‌اند.
  • Test Runner: کامپوننتی که اجرای تست‌ها را مدیریت کرده و خروجی را به کاربر یا فایل گزارش ارائه می‌دهد.

نوشتن اولین تست واحد با یک سناریوی واقعی

آموزش unittest در پایتون + نمونه کد تست واحد — تصویر 3

برای درک ملموس موضوع، یک سیستم مدیریت حساب بانکی ساده را در نظر بگیرید. ابتدا کد ماژول اصلی را تعریف می‌کنیم و سپس تست‌های مربوط به آن را می‌نویسیم.

فرض کنید کدی مشابه اسنیپت‌های پایتون زیر برای مدیریت حساب در اختیار دارید:

# bank_account.py

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

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

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

حالا تست‌های واحد مربوط به این کلاس را در فایلی جداگانه به نام test_bank_account.py پیاده‌سازی می‌کنیم:

# test_bank_account.py
import unittest
from bank_account import BankAccount

class TestBankAccount(unittest.TestCase):

    def test_deposit_successful(self):
        account = BankAccount("علی", 1000.0)
        new_balance = account.deposit(500.0)
        self.assertEqual(new_balance, 1500.0)
        self.assertEqual(account.balance, 1500.0)

    def test_withdraw_insufficient_funds(self):
        account = BankAccount("رضا", 200.0)
        with self.assertRaises(ValueError) as context:
            account.withdraw(500.0)
        self.assertEqual(str(context.exception), "موجودی کافی نیست.")

if __name__ == "__main__":
    unittest.main()

متدهای کلیدی Assertion در unittest

آموزش unittest در پایتون + نمونه کد تست واحد — تصویر 4

متدهای Assertion ابزارهایی هستند که صحت فرضیات ما را در تست ارزیابی می‌کنند. اگر شریط تعیین شده برقرار نباشد، تست شکست می‌خورد (Fail می شود).

متد Assertion کاربرد و توضیحات
self.assertEqual(a, b) بررسی برابر بودن دو مقدار a و b
self.assertNotEqual(a, b) بررسی نابرابر بودن دو مقدار
self.assertTrue(x) / assertFalse(x) ارزیابی صحیح (True) یا غلط (False) بودن یک عبارت منطقی
self.assertIsNone(x) / assertIsNotNone(x) بررسی اینکه مقدار یک متغیر برابر با None است یا خیر
self.assertIn(item, list) بررسی وجود یک عنصر در لیست، رشته یا دیکشنری
self.assertRaises(Exception) تأیید رخ دادن یک خطا (Exception) هنگام اجرای تابع
self.assertAlmostEqual(a, b, places=2) مقایسه اعداد اعشاری با دقت مشخص (جلوگیری از خطای گرد کردن float)

مدیریت چرخه حیات تست با Fixtures (setUp و tearDown)

آموزش unittest در پایتون + نمونه کد تست واحد — تصویر 5

برای جلوگیری از تکرار کد (DRY) در آماده‌سازی داده‌های تست، متدهای متادیتا و متدولوژی Fixtures به کار می‌روند. چهار متد اصلی در unittest وجود دارد که چرخه حیات تست را کنترل می‌کنند:

  1. setUp: قبل از اجرای هر متد تست اجرا می‌شود.
  2. tearDown: بلافاصله بعد از اجرای هر متد تست اجرا می‌شود (حتی اگر تست خطا بدهد).
  3. setUpClass: دقیقاً یک‌بار قبل از شروع تمام متدهای کلاس تست اجرا می‌شود (نیاز به دکوراتور @classmethod دارد).
  4. tearDownClass: دقیقاً یک‌بار پس از اتمام تمامی تست‌های کلاس اجرا می‌شود.
import unittest

class TestDatabaseOperations(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        # اتصال سنگین به دیتابیس برای کل کلاس فقط یک بار
        cls.db_connection = "Connected to DB"

    def setUp(self):
        # ساخت یک شیء تمیز قبل از هر تست
        self.account = {"name": "سارا", "balance": 100}

    def tearDown(self):
        # پاک‌سازی تغییرات بعد از هر تست
        self.account.clear()

    def test_account_has_balance(self):
        self.assertGreater(self.account["balance"], 0)

    @classmethod
    def tearDownClass(cls):
        # بستن اتصال دیتابیس
        cls.db_connection = None

تکنیک‌های پیشرفته: استفاده از Mocking برای جداسازی وابستگی‌ها

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

مثال: فرض کنید تابع ما اطلاعات آب‌وهوا را از یک API خارجی دریافت می‌کند. ما می‌خواهیم بدون ارسال درخواست واقعی HTTP، رفتار تابع را تست کنیم:

import unittest
from unittest.mock import patch
import requests

def get_weather(city):
    response = requests.get(f"https://api.weather.com/v1/{city}")
    if response.status_code == 200:
        return response.json()["temp"]
    return None

class TestWeatherAPI(unittest.TestCase):

    @patch('requests.get')
    def test_get_weather_success(self, mock_get):
        # تنظیم رفتار شبیه‌سازی‌شده (Mock)
        mock_get.return_value.status_code = 200
        mock_get.return_value.json.return_value = {"temp": 25}

        result = get_weather("Tehran")

        self.assertEqual(result, 25)
        # اطمینان از اینکه متد با آدرس درست فراخوانی شده است
        mock_get.assert_called_once_with("https://api.weather.com/v1/Tehran")

if __name__ == "__main__":
    unittest.main()

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

اجرای تست‌ها از طریق خط فرمان گزینه‌های متنوعی را برای سفارشی‌سازی خروجی در اختیار توسعه‌دهنده قرار می‌دهد:

  • اجرای یک فایل تست مشخص:

    python -m unittest test_bank_account.py
  • اجرای جزییات با حالت Verbose (نمایش نام تک‌تک تست‌ها):

    python -m unittest -v test_bank_account.py
  • کشف و اجرای خودکار تمامی تست‌های پروژه (Test Discovery):

    python -m unittest discover -s tests -p "test_*.py"

۵ اشتباه مهلک در نوشتن تست‌های واحد پایتون

  • وابستگی تست‌ها به یکدیگر: تست‌ها باید مستقل باشند و ترتیب اجرای آن‌ها نباید روی نتیجه تأثیر بگذارد.
  • تست کردن جزئیات پیاده‌سازی به جای خروجی: تست باید رفتارهای ورودی و خروجی (Public API) را بسنجد، نه متدهای خصوصی داخلی را.
  • استفاده از دیتابیس و شبکه واقعی: تست واحد باید سریع باشد. استفاده از منابع واقعی سرعت اجرای تست‌ها را به شدت کاهش داده و باعث Flaky شدن (خطاهای ناپایدار) تست‌ها می‌شود.
  • فراموش کردن تست حالت‌های مرزی (Edge Cases): نوشتن تست فقط برای ورودی‌های درست کافی نیست؛ باید مقادیر صفر، منفی، خالی و Exceptionها نیز تست شوند.
  • عدم پاک‌سازی وضعیت (State Leakage): تغییر داده‌ها در setUpClass بدون بازگرداندن به حالت اولیه در tearDownClass می‌تواند تست‌های بعدی را خرای کند.

عیب‌یابی سریع (Troubleshooting)

مشکل ۱: دستور unittest هیچ تستی را پیدا نمی‌کند (Ran 0 tests).
علت: عدم رعایت الگوی نام‌گذاری پایتون.
حل مشکل: مطمئن شوید نام فایل‌ها با test_ شروع می‌شود و متدهای داخل کلاس تست نیز حتماً پیشوند test_ دارند.

مشکل ۲: خطای ModuleNotFoundError هنگام اجرای unittest discover.
علت: عدم شناسایی مسیر کدهای اصلی توسط پایتون.
حل مشکل: مطمئن شوید در پوشه‌ها فایل __init__.py وجود دارد یا متغیر محیطی PYTHONPATH=. را هنگام اجرای دستور تنظیم کرده‌اید.

مشکل ۳: خطاهای عجیب هنگام کار با patch در Mocking.
علت: Patch کردن در محل تعریف به جای محل استفاده.
حل مشکل: همیشه ماشینی که کد در آن فراخوانی می‌شود را Patch کنید، نه ماژولی که تابع در آن تعریف شده است.

پرسش‌های متداول

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

چگونه یک تست خاص را نادیده بگیریم یا Skip کنیم؟
می‌توانید از دکوراتور @unittest.skip("علت نادیده گرفتن") یا @unittest.skipIf(condition, "علت") بالای متد تست استفاده کنید.

آیا می‌توان از unittest برای تست توابع Async (همزمانی) استفاده کرد؟
بله، از نسخه ۳.۸ پایتون به بعد، کلاس unittest.IsolatedAsyncioTestCase برای نوشتن تست‌های async با async/await به صورت توکار اضافه شده است.

پوشش کد (Code Coverage) در unittest چگونه محاسبه می‌شود؟
برای بررسی میزان پوشش کدهای پروژه توسط تست‌ها، می‌توانید ابزار coverage را نصب کرده و دستور coverage run -m unittest discover را اجرا کنید.

سوالات متداول

کتابخانه unittest در پایتون چیست؟

کتابخانه unittest ماژول توکار (Built-in) پایتون برای نوشتن تست‌های واحد خودکار است. این ماژول بدون نیاز به نصب بسته‌های جانبی، امکان سنجش صحت عملکرد توابع و کلاس‌ها را فراهم می‌کند.

تفاوت setUp و setUpClass در unittest چیست؟

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

چگونه استثناها (Exceptions) را در unittest تست کنیم؟

برای تست وقوع خطاهای مورد انتظار می‌توانید از مدیریت‌کننده بافت with self.assertRaises(Exception) در ساختار تست خود استفاده کنید.

آیا برای استفاده از unittest نیاز به نصب پکیج جداگانه داریم؟

خیر، کتابخانه unittest به صورت استاندارد و توکار همراه با پایتون نصب می‌شود و نیاز به نصب هیچ ابزار جانبی ندارد.

Table of Contents

آخرین نوشته‌ها

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *