تا اینجای کار، تمام اجزای سیستم کیف پول ما در یک محیط بسته و تحت کنترل خودمان تست شدند. اما در دنیای واقعی، نرمافزارها منزوی نیستند و باید با سرویسهای بیرونی ارتباط برقرار کنند. فرض کنید میخواهیم قابلیت جدیدی به کیف پول اضافه کنیم تا قیمت لحظهای دلار را از یک API اینترنتی بگیرد و معادل ریالی موجودی کاربر را حساب کند.
اینجاست که با یک چالش بزرگ مواجه میشویم. اگر موقع اجرای تستها اینترنت قطع باشد، چه اتفاقی میافتد؟ یا اگر سرور قیمت دلار کند باشد یا کلاً از دسترس خارج شود، تکلیف ارزیابی کدهای ما چیست؟ آیا درست است که صحت کارکرد کیف پول ما به عوامل بیرونی و غیرقابل پیشبینی گره بخورد؟ قطعاً خیر. تستهای واحد باید سریع، مستقل و تحت هر شرایطی تکرارپذیر باشند.
پاسخ علمی و استاندارد مهندسی نرمافزار برای حل این مشکل، استفاده از تکنیکی به نام Mocking یا شبیهسازی است. ماک کردن به ما اجازه میدهد یک شیء یا تابع فرضی بسازیم که رفتار سرویس بیرونی را تقلید کند. با این روش، بدون اینکه واقعاً به اینترنت متصل شویم یا درخواستی بفرستیم، پاسخ فرضی سرور را بازسازی میکنیم.
اصل حرف این است که با ابزار شبیهسازی در pytest، ما مرز کدهای خودمان را با جهان بیرون مشخص میکنیم. در این درس یاد میگیریم که چطور با استفاده از کتابخانههای استاندارد پایتون، جلوی خرابکاریهای احتمالی سرویسهای بیرونی را در فرآیند تستنویسی بگیریم و کدهایی پایدار خلق کنیم.
مفهوم وابستگی (Dependency) و چرا به ماک کردن نیاز داریم؟
هر قطعه کد در یک پروژه بزرگ، وظیفه مشخصی دارد؛ اما گاهی برای انجام کارش مجبور است به ابزارهای خارج از کنترل خود تکیه کند. در مهندسی نرمافزار، وقتی کدهای بخش اول برای اجرای درست به بخش دیگری نیاز داشته باشند، میگوییم بخش اول به بخش دوم وابستگی (Dependency) دارد. در سناریوی پروژه ما، تابع محاسبه قیمت ریالی برای کارکرد درست، کاملاً به API آنلاین قیمت دلار وابسته است.
این وابستگیها در محیط عملیاتی عالی هستند، اما در زمان ارزیابی کدهای پایتون به یک کابوس تبدیل میشوند. اگر برای تست کردن کدهای خود مستقیماً به ابزارهای بیرونی متصل شوید، چند مشکل اساسی پیش میآید. اول اینکه سرعت اجرای تستها به شدت افت میکند، چون فرستادن درخواست در شبکه زمانبر است. دوم اینکه اگر سرور مقصد به دلیل اختلالات فنی قطع شود، تست شما با خطا مواجه میشود، در حالی که کدهای خودتان کاملاً سالم و بدون عیب هستند.
هدف اصلی در تستهای واحد (Unit Tests)، ایزوله کردن کدهای خودمان از تمام عوامل ناپایدار بیرونی است. ما میخواهیم مطمئن شویم منطق ریاضی و ساختار منطقی کدهایی که نوشتهایم درست کار میکند. ابزار ارزیابی نباید به خاطر قطع شدن شبکه یا بهروزرسانی سرور بانک، فرآیند توسعه شما را متوقف کند.
اینجاست که نیاز به شبیهسازی یا همان ماک کردن مشخص میشود. ما با این تکنیک، تمام وابستگیهای خارجی ناپایدار را قطع میکنیم و یک ماکت بیخطر و قابل پیشبینی جایگزین آنها میکنیم تا فرآیند ارزیابی نرمافزار با سرعت بالا و در محیطی کاملاً ایزوله جلو برود.
آشنایی با Mock و مرزهای شبیهسازی در پایتون
پایتون برای پیادهسازی مفهوم شبیهسازی، یک ابزار استاندارد و بومی به نام کتابخانه unittest.mock دارد. قلب تپنده این کتابخانه، کلاسی به نام Mock است. وقتی شما یک نمونه از این کلاس میسازید، در واقع یک شیء همهفنحریف و توخالی ایجاد کردهاید که هیچ رفتار پیشفرضی ندارد، اما آمادگی دارد تا نقش هر چیزی را در پروژه بازی کند.
بذار ساده بگم؛ شیء Mock مثل یک بازیگر تئاتر حرفهای است. اگر به او بگویید نقش یک تابع اتصال به دیتابیس را بازی کن، این کار را میکند. اگر بگویید نقش یک ماژول ارسال پیامک را بازی کن، باز هم ژست آن را به خود میگیرد. ویژگی شگفتانگیز کلاس Mock این است که در برابر هر نوع صدا زدن متدها یا دسترسی به ویژگیها (Attributes) چراغ سبز نشان میدهد و هیچوقت خطای عدم وجود متد (AttributeError) نمیدهد.
به این نمونه کد ساده نگاه کنید تا متوجه انعطافپذیری این ابزار شوید:
from unittest.mock import Mock
# ساخت یک شیء شبیهساز خام
fake_service = Mock()
# صدا زدن یک متد کاملاً ساختگی که اصلاً وجود ندارد
result = fake_service.get_live_dollar_price()
print(result) # خروجی: <Mock name='mock.get_live_dollar_price()' id='...'>
همانطور که میبینید، پایتون بدون هیچ اعتراضی یک شیء ماک جدید در پاسخ برمیگرداند. اما مرز این شبیهسازی کجاست؟ ابزار Mock نه تنها رفتار شیء واقعی را تقلید میکند، بلکه تمام تعاملات شما را هم در حافظه خودش ثبت میکند. این یعنی بعد از اجرای تست، شما میتوانید از این شیء بپرسید: «آیا فلان تابع صدا زده شد؟»، «چند بار صدا زده شد؟» و «چه ورودیهایی به آن پاس داده شد؟».
اصل حرف این است که در مهندسی نرمافزار، ما از Mock استفاده میکنیم تا بدون درگیر شدن با پیچیدگیهای داخلی یک سرویس، فقط پوسته بیرونی و رفتار رفتوبرگشتی آن را کنترل کنیم. این کار کدهای تست ما را به شدت سریع و مستقل نگه میدارد.
جادوی دکوراتور patch؛ چطور یک تابع بیرونی را جایگزین کنیم؟
ساختن یک شیء Mock در حافظه قدم اول است، اما چالش اصلی اینجاست: چطور این دستساز خودمان را به مغز پایتون تزریق کنیم تا بجای تابع واقعی اجرا شود؟ پاسخ این معما در دکوراتور قدرتمند patch نهفته است. این ابزار که در ماژول unittest.mock قرار دارد، مثل یک جراح دقیق عمل میکند. او مسیر اتصال کدهای شما به دنیای بیرون را به طور موقت میبرد، تابع واقعی را برمیدارد و شیء Mock را جای آن میکارد.
بذار ساده بگم؛ کار دکوراتور patch این است که در طول مدت اجرای یک تست مشخص، آدرسدهی پایتون را فریب دهد. به محض اینکه تست تمام شود، همهچیز به حالت عادی برمیگردد و تابع اصلی سر جایش ریست میشود. این یعنی کدهای شما در محیط واقعی دستنخورده میمانند و دستکاری فقط در زمان ارزیابی نرمافزار رخ میدهد.
به این سناریو نگاه کنید. فرض کنید در فایل wallet.py تابعی به نام get_usd_price داریم که از کتابخانه requests برای گرفتن قیمت استفاده میکند. با این روش آن را جایگزین میکنیم:
from unittest.mock import patch
import wallet
# آدرس دقیق تابعی که باید جراحی و جایگزین شود را میدهیم
@patch('wallet.get_usd_price')
def test_calculate_currency(mock_get_price):
# پایتون به طور خودکار شیء شبیهساز را به عنوان آرگومان به تست پاس میدهد
assert mock_get_price is not None
یک نکته مهندسی که باید به آن دقت کنید ورودی mock_get_price در تابع تست است. وقتی بالای یک تست از patch استفاده میکنید، این دکوراتور کنترلِ آن بخش را در دست میگیرد و شیء شبیهسازیشده را به عنوان یک متغیر ورودی به تابع تست شما تزریق میکند. حالا شما درون بدنه تست، قدرت کامل را برای مدیریت رفتارهای این تابع در دست دارید.
اصل حرف این است که با دکوراتور patch، ما بدون تغییر دادن حتی یک خط از کدهای اصلی پروژه، رفتار آن را در محیط تست بازنویسی میکنیم. این کار مرز بین کدهای ما و وابستگیهای خارجی را کاملاً شفاف و تحت کنترل نگه میدارد.
مدیریت پاسخها با return_value و side_effect
جایگزین کردن یک تابع با دکوراتور patch فقط قدم اول است. حالا باید به این شیء شبیهسازیشده یاد بدهیم که در جواب کدهای ما چه چیزی برگرداند. کتابخانه استاندارد پایتون برای مدیریت پاسخهای یک شیء Mock، دو ابزار کلیدی و بسیار قدرتمند به نامهای return_value و side_effect در اختیار ما میگذارد.
ویژگی اول یعنی return_value برای سناریوهای قابل پیشبینی و خطی است. وقتی از این ویژگی استفاده میکنید، به شیء ماک دستور میدهید که هر زمان و تحت هر شرایطی صدا زده شد، یک مقدار ثابت و مشخص را در خروجی تحویل دهد.
به این نمونه کد نگاه کنید:
from unittest.mock import Mock
mock_api = Mock()
# تعیین مقدار بازگشتی ثابت
mock_api.return_value = 55000
# صدا زدن تابع شبیهسازیشده
print(mock_api()) # خروجی: 55000
نوشتن اولین تست واقعی برای قابلیت تبدیل ارز کیف پول
با return_value ما یک پاسخ قطعی و بدون دردسر میسازیم. مثلاً فرض میکنیم قیمت دلار همیشه ۵۵ هزار تومان است و منطق کیف پول را با این عدد ثابت میسنجیم.
اما دنیای واقعی همیشه اینقدر آرام و خطی نیست. گاهی نیاز داریم رفتار پیچیدهتری را شبیهسازی کنیم؛ مثلاً میخواهیم با اولین فراخوانی یک عدد برگردد، با دومین فراخوانی عددی دیگر، و با سومین فراخوانی کلاً خطای شبکه رخ دهد. اینجاست که باید سراغ side_effect برویم. این ویژگی ابزار ارزیابی نرمافزار را وارد سطح جدیدی از هوشمندی میکند.
به این سناریوی پیشرفته نگاه کنید:
from unittest.mock import Mock
mock_service = Mock()
# تزریق یک لیست از پاسخها و خطاها
mock_service.side_effect = [55000, 56000, ConnectionError("شبکه قطع است!")]
print(mock_service()) # فراخوانی اول: 55000
print(mock_service()) # فراخوانی دوم: 56000
print(mock_service()) # فراخوانی سوم: خطای ارتباطی رخ میدهد!
اصل حرف این است که return_value برای شبیهسازی روزهای آفتابی و پاسخهای موفق سیستم است، اما side_effect ابزار مهندسی شما برای شبیهسازی روزهای طوفانی، خطاهای سرور و رفتارهای متغیر پایتون است. با ترکیب این دو ویژگی، کدهای تست شما توانایی مواجهه با هر شرایطی را پیدا میکنند.
اشتباهات رایج: خطای آدرسدهی اشتباه در patch (کجا را باید ماک کنیم؟)
یکی از رایجترین و کلافهکنندهترین خطاهایی که توسعهدهندگان پایتون هنگام کار با دکوراتور patch با آن مواجه میشوند، این است که همه مراحل را درست پیاده کردهاند، اما شبیهساز کار نمیکند و تابع واقعی اجرا میشود. داکیومنت رسمی pytest و پایتون علت این مشکل را در یک قانون طلایی خلاصه میکنند: «همیشه جایی را ماک کنید که تابع در آنجا استفاده میشود، نه جایی که تابع در آنجا تعریف شده است». (Mock where it is used, not where it is defined).
بذار ساده بگم؛ فرض کنید تابع get_usd_price در ماژولی به نام services.py نوشته شده است. شما این تابع را در فایل wallet.py وارد (import) کردهاید تا موجودی را حساب کنید. حالا اگر در فایل تست بنویسید @patch('services.get_usd_price')، شبیهسازی شما هیچ اثری روی پروژه نمیگذارد و شکست میخورد.
چرا این اتفاق میافتد؟ وقتی فایل wallet.py اجرا میشود، پایتون در همان ابتدا یک نسخه از تابع را در حافظه خودش و در فضای نام (Namespace) اختصاصیِ کیف پول کپی میکند. وقتی شما آدرس services را ماک میکنید، مکان اصلی را تغییر دادهاید، اما کدهای کیف پول شما همچنان دارند از آن کپیِ محلی که در فضای نام خودشان ذخیره شده استفاده میکنند.
برای حل این چالش، باید آدرسدهی را مستقیماً بر اساس محل مصرف تنظیم کنید:
# اشتباه: تغییر منبع اصلی (تست کار نمیکند)
@patch('services.get_usd_price')
# درست: تغییر محل مصرف تابع در پروژه (تست موفق میشود)
@patch('wallet.get_usd_price')
def test_wallet_conversion(mock_price):
...
اصل حرف این است که پایتون به مسیر فراخوانی نهایی نگاه میکند. برای جلوگیری از این اشتباه، همیشه از خودتان بپرسید: «کدهای پروژه من در کدام فایل دارند این وابستگی را صدا میزنند؟». پاسخ هر چه بود، همان فایل را هدف دکوراتور patch قرار دهید تا ارزیابی نرمافزار شما کاملاً دقیق و بدون رفتار غیرمنتظره جلو برود.