Python Coding Academy
135 subscribers
7 photos
3 files
4 links
🐍 Learn Python. Build Skills. Create the Future.

📚 Python lessons from beginner to advanced
💻 Clear explanations & real-world examples
🚀 Learn to code. Learn to build.
Download Telegram
🐍 آموزش جامع Python — پست ۲۴۰
ثبت خطاها با ماژول logging
در پست قبل با Exceptionهای اختصاصی آشنا شدیم.
حالا می‌خواهیم یاد بگیریم چگونه اتفاقات مهم برنامه و خطاها را ثبت کنیم تا هنگام عیب‌یابی بتوانیم آن‌ها را بررسی کنیم.
برای این کار از ماژول استاندارد logging استفاده می‌کنیم.
🔹 اولین Log
import logging

logging.basicConfig(level=logging.INFO)

logging.info("Application started")
logging.warning("Low disk space")
logging.error("Database connection failed")

خروجی نمونه:
INFO:root:Application started
WARNING:root:Low disk space
ERROR:root:Database connection failed

سطح‌های مهم Log عبارت‌اند از:
سطح
کاربرد
DEBUG
اطلاعات جزئی برای عیب‌یابی
INFO
رویدادهای عادی برنامه
WARNING
وضعیت غیرمنتظره اما قابل ادامه
ERROR
خطا در یک عملیات
CRITICAL
مشکل بسیار جدی
🔹 ثبت Exception
فرض کنیم هنگام اتصال به دیتابیس خطایی رخ می‌دهد:
import logging
import sqlite3

logging.basicConfig(level=logging.INFO)

try:
connection = sqlite3.connect("app.db")
connection.execute("SELECT * FROM missing_table")

except sqlite3.Error:
logging.exception("Database operation failed")

finally:
if "connection" in locals():
connection.close()

متد logging.exception() پیام خطا را همراه با اطلاعات Exception و Traceback ثبت می‌کند.
این متد را معمولاً داخل بلوک except استفاده می‌کنیم.
🔹 ذخیره Log در فایل
برای ثبت گزارش در فایل:
import logging

logging.basicConfig(
filename="app.log",
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
encoding="utf-8"
)

logging.info("Application started")
logging.error("Operation failed")

اکنون پیام‌ها در فایل app.log ذخیره می‌شوند.
گزینه encoding="utf-8" برای ثبت درست متن فارسی مفید است.
🔹 چرا print() کافی نیست؟
print() برای نمایش خروجی مناسب است، اما logging امکانات بیشتری دارد:
سطح‌بندی پیام‌ها
ثبت زمان رویداد
ذخیره گزارش در فایل
ثبت Traceback خطاها
کنترل میزان جزئیات گزارش
در برنامه‌های واقعی، Logging به عیب‌یابی و نگهداری بهتر کمک می‌کند.
⚠️ نکته مهم
logging.basicConfig() معمولاً فقط زمانی تنظیمات اصلی را اعمال می‌کند که Root Logger از قبل Handler نداشته باشد.
همچنین رمز عبور، کلید API و اطلاعات حساس کاربران را در Log ثبت نکن؛ فایل‌های گزارش نیز باید از نظر دسترسی محافظت شوند.
🧠 خلاصه
DEBUG
INFO
WARNING
ERROR
CRITICAL

برای ثبت Exception همراه با Traceback:
logging.exception("Operation failed")

و برای ذخیره گزارش در فایل:
logging.basicConfig(filename="app.log")

🎯 تمرین
برنامه‌ای بنویس که:
گزارش‌ها را در فایل database.log ذخیره کند.
شروع برنامه را با INFO ثبت کند.
یک خطای آزمایشی ValueError ایجاد کند.
خطا را با logging.exception() ثبت کند.
اجرای برنامه را با یک پیام INFO گزارش دهد.
پست ۲۴۰ از دوره جامع Python 🐍
👍1
🐍 آموزش جامع Python — پروژه عملی شماره ۱
ساخت سیستم مدیریت کاربران با SQLite
در این پروژه قرار است یک برنامه نسبتاً واقعی بسازیم که کاربران را در دیتابیس ذخیره کند، اطلاعاتشان را مدیریت کند و خطاها را ثبت کند.
🎯 هدف پروژه: ترکیب مفاهیمی که تا اینجا یاد گرفته‌ایم در یک برنامه کاربردی.
🔹 امکانات برنامه
برنامه باید بتواند:
کاربر جدید ثبت کند.
تمام کاربران را نمایش دهد.
کاربر را با شناسه پیدا کند.
اطلاعات کاربر را ویرایش کند.
کاربر را حذف کند.
ورودی‌ها را اعتبارسنجی کند.
خطاها را مدیریت و در فایل ثبت کند.
اطلاعات را در SQLite نگهداری کند تا با بستن برنامه از بین نروند.
🔹 ساختار پروژه
user_manager/
│
├── main.py
├── database.py
├── user_repository.py
├── exceptions.py
└── app.log

وظیفه هر فایل:
main.py — منوی برنامه و دریافت ورودی
database.py — ایجاد اتصال به دیتابیس
user_repository.py — عملیات CRUD
exceptions.py — تعریف خطاهای اختصاصی
app.log — ذخیره گزارش خطاها و رویدادها
🔹 مرحله اول: طراحی جدول
جدول users باید این ستون‌ها را داشته باشد:
ستون
نوع
محدودیت
id
INTEGER
کلید اصلی
username
TEXT
اجباری و یکتا
age
INTEGER
بین ۱ تا ۱۲۰
از محدودیت‌های PRIMARY KEY، NOT NULL، UNIQUE و CHECK استفاده کن.
🔹 مرحله دوم: پیاده‌سازی Repository
در کلاس UserRepository این متدها را پیاده‌سازی کن:
create_table()
add_user(username, age)
get_all()
get_by_id(user_id)
update_user(user_id, username, age)
delete_user(user_id)

قوانین:
نام کاربری نباید خالی باشد.
نام کاربری تکراری نباید ثبت شود.
سن باید عدد صحیح بین ۱ تا ۱۲۰ باشد.
شناسه باید عدد صحیح مثبت باشد.
برای تمام Queryهای دارای ورودی از Placeholder یعنی ? استفاده کن.
🔹 مرحله سوم: مدیریت خطا
در فایل exceptions.py دست‌کم این خطاها را تعریف کن:
class UserError(Exception):
pass


class InvalidUserError(UserError):
pass


class UserNotFoundError(UserError):
pass


class DuplicateUserError(UserError):
pass

خطاها باید متناسب با شرایط ایجاد شوند و در main.py به شکل مناسب مدیریت شوند.
برای خطای تکراری بودن نام کاربری می‌توانی از sqlite3.IntegrityError استفاده کنی؛ اما فقط زمانی آن را به DuplicateUserError تبدیل کن که علت نقض محدودیت واقعاً تکراری بودن نام باشد.
🔹 مرحله چهارم: ثبت گزارش
با logging رویدادهای مهم را در app.log ثبت کن:
INFO     User created
INFO User updated
INFO User deleted
WARNING Invalid input
ERROR Database operation failed

اطلاعات حساس را در گزارش‌ها ذخیره نکن.
🔹 مرحله پنجم: منوی برنامه
منوی زیر را پیاده‌سازی کن:
===== User Manager =====

1. Add user
2. Show all users
3. Find user
4. Update user
5. Delete user
0. Exit

برنامه باید تا زمانی که کاربر گزینه خروج را انتخاب نکرده، به اجرای منو ادامه دهد.
⚠️ قوانین مهم پروژه
کد را بین فایل‌های مشخص‌شده تقسیم کن.
از try/except برای مدیریت خطاهای مورد انتظار استفاده کن.
اتصال دیتابیس را به‌درستی ببند.
تراکنش‌ها را مدیریت کن تا تغییرات ناقص ثبت نشوند.
از تکرار بی‌دلیل Queryها جلوگیری کن.
اعتبارسنجی Python را در کنار محدودیت‌های SQLite انجام بده.
🎯 معیار تکمیل پروژه
پروژه زمانی کامل است که بتوانی:
دو کاربر متفاوت ثبت کنی.
کاربر تکراری را رد کنی.
اطلاعات یک کاربر را تغییر بدهی.
کاربر مشخصی را حذف کنی.
شناسه نامعتبر را مدیریت کنی.
برنامه را ببندی و دوباره اجرا کنی، بدون اینکه اطلاعات قبلی از بین بروند.
گزارش خطاها را در app.log مشاهده کنی.
پست پروژه‌ای از دوره جامع Python 🐍
1👍1
هیچ صحنه ای توی دنیا
اندازه این صحنه که چیزی در حال لود شدن یا هر چیز دیگه ای روی اعصابم راه نمیره
👍1
📢 اطلاعیه درباره روند انتشار آموزش‌های Python 🐍
دوستان عزیز، همراهان دوره جامع Python،
با توجه به حجم مطالبی که تا اینجا در دوره منتشر شده، تصمیم گرفتیم فعلاً سرعت انتشار آموزش‌های جدید را کمی کاهش دهیم تا فرصت کافی برای مطالعه، مرور و تمرین مطالب قبلی داشته باشید.
🎯 هدف ما فقط انتشار مطالب بیشتر نیست؛ هدف این است که مطالب را به‌خوبی یاد بگیرید و بتوانید در پروژه‌های واقعی از آن‌ها استفاده کنید.
پیشنهاد می‌کنیم در این مدت:
🔹 پست‌های قبلی را با دقت مرور کنید.
🔹 مثال‌های کد را خودتان اجرا کنید.
🔹 تمرین‌های هر بخش را حل کنید.
🔹 سعی کنید بدون نگاه کردن به آموزش، کدها را دوباره بنویسید.
🔹 اگر بخشی را خوب متوجه نشده‌اید، به همان قسمت برگردید و دوباره مطالعه کنید.
⏳ این کاهش سرعت موقتی است و به معنی توقف دوره نیست. آموزش‌ها ادامه خواهند داشت؛ فقط می‌خواهیم فرصت بیشتری برای یادگیری عمیق‌تر و تثبیت مطالب داشته باشید.
از همراهی و حمایت شما سپاسگزاریم. ❤️
آرام‌تر پیش رفتن، اما عمیق‌تر یاد گرفتن؛ اصل مهم این مسیر است.
🐍 آموزش جامع Python
👍1
🔥 یه کانال پیدا کردم که از بچه‌های همین کانال خودمونه! 😎

داخلش هم چیزای خوب و کاربردی پیدا میشه، هم یه سری چیزای عجیب و باحال 🤯

مثلاً تو یکی از پست‌هاش نوشته: «آموزش داشتن دو تا سیستم‌عامل برای کامپیوتر» 😳
مگه همچین چیزی داریم؟! 😅

شاید داشته باشیم… 😏
خواستید یه سر بزنید، ضرر نمی‌کنید 👇
https://t.me/whyitinfo

هر چی تو عمرم ندیده بودم اینجا دیدم 😁
🐍 آموزش جامع Python
10👍2
🐍 آموزش جامع Python — پست ۲۴۱
ساخت تست‌های یکپارچه برای Repository در SQLite
در پست‌های قبلی یاد گرفتیم چطور با Repository Pattern عملیات دیتابیس را از منطق اصلی برنامه جدا کنیم و با unittest تست بنویسیم.
حالا می‌خواهیم بررسی کنیم که آیا چند بخش برنامه، وقتی با هم کار می‌کنند، درست عمل می‌کنند یا نه. به این روش Integration Testing می‌گوییم.
🔹 تفاوت تست واحد و تست یکپارچه
Unit Test: یک بخش کوچک از برنامه را به‌صورت مستقل بررسی می‌کند.
Integration Test: تعامل چند بخش را با یکدیگر بررسی می‌کند؛ مثلاً Repository و دیتابیس SQLite.
🔹 مثال عملی
فرض کنیم فایل user_repository.py شامل کلاس زیر است:
class UserRepository:

def __init__(self, connection):
self.connection = connection

def create_table(self):
self.connection.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
)
""")

def add_user(self, name):
cursor = self.connection.execute(
"INSERT INTO users (name) VALUES (?)",
(name,)
)
return cursor.lastrowid

def get_by_id(self, user_id):
return self.connection.execute(
"SELECT id, name FROM users WHERE id = ?",
(user_id,)
).fetchone()

حالا یک فایل به نام test_user_repository.py بساز:
import sqlite3
import unittest

from user_repository import UserRepository


class TestUserRepository(unittest.TestCase):

def setUp(self):
self.connection = sqlite3.connect(":memory:")
self.connection.row_factory = sqlite3.Row

self.repository = UserRepository(self.connection)
self.repository.create_table()

def tearDown(self):
self.connection.close()

def test_add_and_get_user(self):
user_id = self.repository.add_user("Ali")

user = self.repository.get_by_id(user_id)

self.assertIsNotNone(user)
self.assertEqual(user["id"], user_id)
self.assertEqual(user["name"], "Ali")


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

🔹 این تست چه کاری انجام می‌دهد؟
۱. یک دیتابیس موقت در حافظه ایجاد می‌کند.
۲. جدول کاربران را می‌سازد.
۳. کاربر جدیدی اضافه می‌کند.
۴. همان کاربر را با شناسه‌اش می‌خواند.
۵. بررسی می‌کند که شناسه و نام بازیابی‌شده صحیح باشند.
۶. پس از پایان تست، اتصال را می‌بندد.
استفاده از :memory: باعث می‌شود تست به دیتابیس واقعی برنامه وابسته نباشد و اطلاعات آزمایشی در فایل اصلی ذخیره نشوند.
🔹 اجرای تست
در ترمینال، از پوشه پروژه اجرا کن:
python -m unittest -v

اگر تست موفق باشد، نتیجه‌ای مشابه زیر مشاهده می‌کنی:
test_add_and_get_user ... ok

Ran 1 test
OK

⚠️ نکته مهم
این تست فقط تعامل Repository با SQLite را بررسی می‌کند. اگر بعداً منطق اصلی برنامه، اعتبارسنجی یا مدیریت خطاها را نیز اضافه کردی، بهتر است برای آن‌ها تست‌های جداگانه بنویسی.
🧠 خلاصه
در تست یکپارچه، به‌جای بررسی صرف یک تابع، چند بخش واقعی برنامه را کنار هم آزمایش می‌کنیم.
🎯 تمرین
یک تست جدید به نام test_missing_user بنویس که شناسه‌ای ناموجود را به get_by_id() بدهد و بررسی کند که نتیجه None است.
پست ۲۴۱ از دوره جامع Python 🐍
👍1
🐍 آموزش جامع Python — پست ۲۴۲
استفاده از unittest.mock برای شبیه‌سازی وابستگی‌ها
در پست قبل تست یکپارچه برای Repository و SQLite نوشتیم.
اما گاهی می‌خواهیم یک بخش از برنامه را بدون اجرای وابستگی‌های واقعی آن آزمایش کنیم. برای این کار می‌توانیم از ماژول استاندارد unittest.mock استفاده کنیم.
به این روش، شبیه‌سازی وابستگی‌ها یا Mocking می‌گوییم.
🔹 چرا Mock لازم است؟
فرض کنید تابعی داریم که برای دریافت اطلاعات به یک سرویس خارجی متصل می‌شود. هنگام تست، شاید نخواهیم واقعاً به آن سرویس متصل شویم.
با Mock می‌توانیم نتیجه مورد انتظار را شبیه‌سازی کنیم و رفتار تابع خودمان را مستقل آزمایش کنیم.
🔹 اولین Mock با Mock
from unittest.mock import Mock

service = Mock()

service.get_username.return_value = "Ali"

result = service.get_username()

print(result)

خروجی:
Ali

در این مثال:
Mock() یک شیء شبیه‌سازی‌شده می‌سازد.
return_value نتیجه‌ای را تعیین می‌کند که فراخوانی متد برمی‌گرداند.
بدون نیاز به پیاده‌سازی واقعی سرویس، می‌توانیم رفتار آن را آزمایش کنیم.
🔹 تست با unittest
فرض کنیم تابع زیر در فایل user_service.py قرار دارد:
def get_display_name(repository, user_id):
user = repository.get_by_id(user_id)

if user is None:
return "Unknown user"

return user["name"]

این تابع به یک Repository وابسته است، اما برای تست آن نیازی به دیتابیس واقعی نداریم.
فایل test_user_service.py:
import unittest
from unittest.mock import Mock

from user_service import get_display_name


class TestUserService(unittest.TestCase):

def test_existing_user(self):
repository = Mock()
repository.get_by_id.return_value = {
"id": 1,
"name": "Ali"
}

result = get_display_name(repository, 1)

self.assertEqual(result, "Ali")
repository.get_by_id.assert_called_once_with(1)

def test_missing_user(self):
repository = Mock()
repository.get_by_id.return_value = None

result = get_display_name(repository, 99)

self.assertEqual(result, "Unknown user")


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

🔹 متد assert_called_once_with()
این متد بررسی می‌کند که یک متد دقیقاً یک بار و با آرگومان‌های مشخص فراخوانی شده باشد:
repository.get_by_id.assert_called_once_with(1)

اگر متد اصلاً فراخوانی نشده باشد، چند بار اجرا شده باشد یا آرگومان دیگری دریافت کرده باشد، تست شکست می‌خورد.
⚠️ نکته مهم
Mock جایگزین تمام تست‌های واقعی نیست. در این مثال بررسی می‌کنیم که منطق get_display_name() درست کار می‌کند؛ اما صحت Queryهای SQLite را بررسی نمی‌کنیم.
بنابراین:
برای منطق مستقل، از Unit Test و Mock استفاده کن.
برای بررسی تعامل واقعی با SQLite، از Integration Test استفاده کن.
🧠 خلاصه
Mock به ما اجازه می‌دهد وابستگی‌های یک تابع را شبیه‌سازی کنیم و رفتار خود تابع را بدون اجرای واقعی آن وابستگی‌ها آزمایش کنیم.
🎯 تمرین
تابعی به نام delete_user_if_exists(repository, user_id) بنویس که:
با repository.get_by_id(user_id) وجود کاربر را بررسی کند.
اگر کاربر وجود داشت، repository.delete_user(user_id) را فراخوانی کند و True برگرداند.
اگر کاربر وجود نداشت، هیچ حذفی انجام ندهد و False برگرداند.
سپس با Mock برای هر دو حالت تست بنویس.
پست ۲۴۲ از دوره جامع Python 🐍
1👍1
🐍 آموزش جامع Python — پست ۲۴۳
تست‌کردن خطاها با assertRaises
در پست‌های قبلی با Exceptionهای اختصاصی و تست‌نویسی با unittest آشنا شدیم.
اما تست موفقیت‌آمیز بودن یک عملیات کافی نیست؛ گاهی باید مطمئن شویم برنامه هنگام دریافت ورودی نامعتبر، خطای درست را ایجاد می‌کند.
برای این کار از assertRaises() استفاده می‌کنیم.
🔹 مثال ساده
import unittest


def divide(a, b):
if b == 0:
raise ValueError("Divisor cannot be zero")

return a / b


class TestDivide(unittest.TestCase):

def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(10, 0)


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

در این مثال، تست زمانی موفق است که فراخوانی divide(10, 0) خطای ValueError ایجاد کند.
اگر تابع هیچ خطایی ایجاد نکند، تست شکست می‌خورد.
🔹 بررسی متن خطا
گاهی فقط نوع خطا کافی نیست و می‌خواهیم پیام آن را هم بررسی کنیم:
def test_error_message(self):
with self.assertRaises(ValueError) as context:
divide(10, 0)

self.assertEqual(
str(context.exception),
"Divisor cannot be zero"
)

متغیر context اطلاعات Exception رخ‌داده را در اختیارمان قرار می‌دهد.
ویژگی context.exception خود شیء خطاست و با str() می‌توانیم پیام آن را بخوانیم.
🔹 کاربرد در پروژه SQLite
فرض کنیم تابعی برای اعتبارسنجی سن کاربران داریم:
def validate_age(age):
if not isinstance(age, int) or isinstance(age, bool):
raise TypeError("Age must be an integer")

if not 1 <= age <= 120:
raise ValueError("Age must be between 1 and 120")

return age

حالا می‌توانیم حالت‌های نامعتبر را تست کنیم:
class TestAgeValidation(unittest.TestCase):

def test_invalid_type(self):
with self.assertRaises(TypeError):
validate_age("twenty")

def test_invalid_range(self):
with self.assertRaises(ValueError):
validate_age(150)

def test_boolean_is_rejected(self):
with self.assertRaises(TypeError):
validate_age(True)

در این تست‌ها بررسی می‌کنیم که ورودی نامعتبر با نوع خطای مورد انتظار رد شود.
🔹 تفاوت assertRaises و try/except
try/except برای مدیریت خطا در اجرای عادی برنامه استفاده می‌شود.
assertRaises بررسی می‌کند که هنگام تست، یک عملیات خطای مشخصی ایجاد کند.
⚠️ نکته مهم
نوع خطای مورد انتظار را دقیق انتخاب کن. اگر انتظار ValueError داری اما برنامه به‌جای آن TypeError ایجاد کند، تست نباید موفق محسوب شود.
همچنین فقط همان دستوری را داخل بلوک assertRaises قرار بده که انتظار داری خطا ایجاد کند؛ در غیر این صورت ممکن است خطای بخش دیگری از کد باعث قبولی اشتباه تست شود.
🧠 خلاصه
with self.assertRaises(ValueError):
some_function()

این الگو برای تست اعتبارسنجی ورودی‌ها، خطاهای اختصاصی و رفتارهای نامعتبر در پروژه‌های Python کاربرد زیادی دارد.
🎯 تمرین
برای تابع validate_age() سه تست بنویس:
۱. سن 25 باید پذیرفته شود.
۲. سن 0 باید ValueError ایجاد کند.
۳. مقدار "25" باید TypeError ایجاد کند.
پست ۲۴۳ از دوره جامع Python 🐍
👍1
🐍 آموزش جامع Python — پست ۲۴۴
استفاده از patch() برای شبیه‌سازی وابستگی‌ها در تست
در پست ۲۴۲ با Mock آشنا شدیم. حالا می‌خواهیم از ابزار patch() استفاده کنیم تا هنگام اجرای تست، یک تابع واقعی را موقتاً با یک نمونه ساختگی جایگزین کنیم.
این کار زمانی مفید است که تابع مورد آزمایش به سرویس ایمیل، API خارجی یا بخش دیگری از برنامه وابسته باشد.
🔹 مثال عملی
فرض کنیم دو فایل داریم.
فایل notifications.py
def send_email(address, message):
print(f"Sending email to {address}")

فایل registration.py
from notifications import send_email


def register_user(email):
# ثبت‌نام کاربر در این قسمت انجام می‌شود
send_email(email, "Welcome!")

تابع register_user() برای ارسال ایمیل به send_email() وابسته است. در تست نمی‌خواهیم ایمیلی واقعاً ارسال شود؛ فقط می‌خواهیم بررسی کنیم که تابع ارسال ایمیل به‌درستی فراخوانی شده است.
🔹 نوشتن تست با patch()
فایل test_registration.py
import unittest
from unittest.mock import patch

from registration import register_user


class TestRegistration(unittest.TestCase):

@patch("registration.send_email")
def test_register_sends_email(self, mock_send_email):
register_user("user@example.com")

mock_send_email.assert_called_once_with(
"user@example.com",
"Welcome!"
)


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

🔍 این کد چگونه کار می‌کند؟
patch() تابع send_email را در ماژول registration موقتاً جایگزین می‌کند.
متغیر mock_send_email نمونه ساختگی ایجادشده را دریافت می‌کند.
assert_called_once_with() بررسی می‌کند که تابع دقیقاً یک بار و با آرگومان‌های مورد انتظار فراخوانی شده باشد.
پس از پایان تست، جایگزینی موقت برداشته می‌شود.
⚠️ نکته بسیار مهم
در این مثال باید بنویسیم:
@patch("registration.send_email")

نه:
@patch("notifications.send_email")

چرا؟ چون در registration.py تابع با دستور from notifications import send_email وارد شده است؛ بنابراین هنگام اجرای register_user()، نام send_email از فضای نام ماژول registration خوانده می‌شود.
قاعده اصلی: چیزی را که کد مورد آزمایش از آن استفاده می‌کند، در همان محل استفاده جایگزین کن.
🎯 تمرین
تابع زیر را در فایل user_service.py در نظر بگیر:
from notifications import send_email


def notify_user(email):
send_email(email, "Your account is ready!")

یک تست بنویس که با patch() ارسال واقعی ایمیل را متوقف کند و بررسی کند که send_email دقیقاً یک بار با آرگومان‌های زیر فراخوانی شده است:
email: مقدار admin@example.com
message: مقدار Your account is ready!
پست ۲۴۴ از دوره جامع Python 🐍
👍1
🐍 آموزش جامع Python — پست ۲۴۵
تفاوت setUp() و tearDown() در تست‌نویسی
وقتی تعداد تست‌ها زیاد می‌شود، ممکن است چند تست به داده‌ها یا منابع اولیه یکسانی نیاز داشته باشند. در کتابخانه unittest می‌توانیم از setUp() و tearDown() برای مدیریت آماده‌سازی و پاک‌سازی هر تست استفاده کنیم.
۱. متد setUp()
این متد پیش از اجرای هر متد تست اجرا می‌شود و برای آماده‌سازی شرایط تست کاربرد دارد.
۲. متد tearDown()
این متد پس از اجرای هر متد تست، در صورت موفقیت یا شکست آن، اجرا می‌شود و برای پاک‌سازی منابع کاربرد دارد؛ البته اگر اجرای setUp() با خطا مواجه شود، tearDown() اجرا نمی‌شود.
🔹 مثال عملی
import unittest


class TestCalculator(unittest.TestCase):

def setUp(self):
self.numbers = [10, 20, 30]
print("Preparing test")

def tearDown(self):
print("Cleaning up test")

def test_sum(self):
self.assertEqual(sum(self.numbers), 60)

def test_length(self):
self.assertEqual(len(self.numbers), 3)


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

🔍 ترتیب اجرا
برای هر تست، ترتیب کلی به این شکل است:
setUp()
↓
اجرای متد تست
↓
tearDown()

بنابراین، برای دو متد تست بالا، setUp() و tearDown() هرکدام دو بار اجرا می‌شوند.
نکته: ترتیب اجرای متدهای تست را نباید بدون دلیل مشخص، ترتیب نوشتن آن‌ها در کلاس فرض کنید.
🔹 چرا این روش مفید است؟
فرض کنید چند تست به یک فهرست اولیه نیاز دارند. به‌جای تکرار ساخت فهرست در هر تست، آن را در setUp() تعریف می‌کنیم.
همچنین هر تست، نمونه جداگانه‌ای از کلاس تست دریافت می‌کند؛ بنابراین تغییر self.numbers در یک تست، به‌طور معمول روی تست دیگر تأثیر نمی‌گذارد.
🔸 تفاوت با setUpClass()
اگر بخواهیم آماده‌سازی فقط یک بار برای کل کلاس تست انجام شود، می‌توانیم از setUpClass() استفاده کنیم. این متد باید با @classmethod تعریف شود.
@classmethod
def setUpClass(cls):
cls.shared_data = [1, 2, 3]

این روش برای منابع پرهزینه‌ای مناسب است که قرار است بین تست‌های یک کلاس مشترک باشند؛ البته باید مراقب تغییر داده‌های مشترک باشیم.
🎯 تمرین
کلاسی به نام TestShoppingCart بساز که:
۱. در setUp() یک سبد خرید با دو محصول ایجاد کند.
۲. در یک تست، تعداد محصولات را بررسی کند.
۳. در تست دیگر، مجموع قیمت محصولات را بررسی کند.
۴. در tearDown() یک پیام برای پایان هر تست چاپ کند.
پست ۲۴۵ از دوره جامع Python 🐍
1👍1
📢 اگر اینترنت بین‌الملل قطع بشه (نت ملی)، ترجیح می‌دید کانال ما در کدوم پیام‌رسان ایرانی فعالیتش رو ادامه بده؟ 🤔
Anonymous Poll
30%
🟠 ایتا
10%
🟣 روبیکا
70%
🟢 بله
0%
🔵 شاد
👍1
چالش برنامه نویسی بدون جایزه! 🔥

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

مثلا برنامتون باید بین این 2 تا کد تفاوت قائل بشه:
#1
print("Hello Dev FA")


#2
while True:
    pass

اما داستان به همینجا ختم نمی‌شه
کدتون باید برای هر برنامه و ورودی دلخواه جواب درست بده و خودشم همیشه تو زمان محدود به جواب برسه
13👍1
🐍 آموزش جامع Python — پست ۲۴۶
مدیریت منابع تست با addCleanup()
در پست قبل با setUp() و tearDown() آشنا شدیم. حالا می‌خواهیم روش دیگری در unittest یاد بگیریم: متد addCleanup().
این متد برای ثبت عملیات پاک‌سازی استفاده می‌شود؛ عملیاتی که پس از پایان تست باید اجرا شوند، مانند بستن فایل یا آزادسازی یک منبع.
🔹 مثال عملی
فرض کنید تست ما یک فایل موقت باز می‌کند. می‌خواهیم مطمئن شویم فایل پس از پایان تست بسته می‌شود.
import unittest


class TestFileHandling(unittest.TestCase):

def setUp(self):
self.file = open("test.txt", "w", encoding="utf-8")

self.addCleanup(self.file.close)

def test_file_is_open(self):
self.assertFalse(self.file.closed)

def test_write_to_file(self):
self.file.write("Hello Python")


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

🔍 کد چگونه کار می‌کند؟
open() فایل را باز می‌کند.
self.addCleanup(self.file.close) تابع بستن فایل را برای مرحله پاک‌سازی ثبت می‌کند.
پس از پایان هر تست، تابع ثبت‌شده اجرا می‌شود؛ حتی اگر تست با شکست مواجه شود یا اجرای آن با خطا متوقف شود.
از آنجا که setUp() برای هر تست اجرا می‌شود، هر تست فایل خودش را باز می‌کند و عملیات پاک‌سازی مربوط به همان فایل را ثبت می‌کند.
🔸 تفاوت addCleanup() با tearDown()
ویژگی
tearDown()
addCleanup()
روش استفاده
بازنویسی یک متد
ثبت یک یا چند تابع
تعداد عملیات پاک‌سازی
می‌توان چند عملیات را در آن نوشت
می‌توان چند تابع جداگانه ثبت کرد
ترتیب اجرا
پس از تست، در صورت موفقیت setUp()
توابع ثبت‌شده با ترتیب معکوس ثبت اجرا می‌شوند
خطا در setUp()
ممکن است اجرا نشود
توابعی که پیش از خطا ثبت شده‌اند، اجرا می‌شوند
نکته مهم: توابع addCleanup() حتی اگر متد تست شکست بخورد یا tearDown() با خطا مواجه شود، اجرا می‌شوند. اگر setUp() قبل از ثبت عملیات پاک‌سازی با خطا مواجه شود، طبیعتاً پاک‌سازی ثبت‌نشده‌ای وجود ندارد.
🎯 تمرین
کلاسی برای تست یک منبع فرضی بساز که:
۱. در setUp() یک فهرست ایجاد کند.
۲. با addCleanup() تابعی را برای پاک‌سازی ثبت کند.
۳. در تابع پاک‌سازی، یک پیام چاپ کند.
۴. بررسی کند که داده‌ها در زمان اجرای تست در دسترس هستند.
هدف این است که تفاوت آماده‌سازی تست و پاک‌سازی خودکار منابع را در عمل درک کنید.
پست ۲۴۶ از دوره جامع Python 🐍
👍1
🐍 آموزش جامع Python — پست ۲۴۷
مدیریت منابع در تست‌ها با setUpClass() و tearDownClass()
در تست‌نویسی، بعضی منابع برای هر تست به‌صورت جداگانه آماده می‌شوند؛ اما بعضی دیگر، مانند یک منبع مشترک یا داده‌های پرهزینه برای آماده‌سازی، بهتر است فقط یک بار برای کل کلاس ایجاد شوند.
در unittest دو متد ویژه برای این کار داریم:
setUpClass() برای آماده‌سازی مشترک پیش از اجرای تست‌های کلاس
tearDownClass() برای پاک‌سازی مشترک پس از پایان اجرای تست‌های کلاس
۱. ساختار اصلی
import unittest


class TestExample(unittest.TestCase):

@classmethod
def setUpClass(cls):
print("Class setup")

@classmethod
def tearDownClass(cls):
print("Class cleanup")

def test_first(self):
self.assertEqual(2 + 3, 5)

def test_second(self):
self.assertTrue(10 > 3)


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

در این مثال، setUpClass() یک بار پیش از اجرای تست‌های این کلاس اجرا می‌شود و tearDownClass() پس از پایان اجرای تست‌های کلاس اجرا خواهد شد.
۲. چرا از @classmethod استفاده می‌کنیم؟
این دو متد به‌جای یک نمونه از کلاس تست، خود کلاس را دریافت می‌کنند؛ بنابراین پارامتر آن‌ها cls است.
@classmethod
def setUpClass(cls):
cls.shared_data = [10, 20, 30]

اکنون داده از طریق کلاس قابل دسترسی است:
self.assertEqual(sum(self.shared_data), 60)

۳. تفاوت با setUp() و tearDown()
متد
زمان اجرا
setUp()
پیش از هر تست
tearDown()
پس از هر تست، اگر setUp() با موفقیت تمام شده باشد
setUpClass()
یک بار پیش از تست‌های کلاس
tearDownClass()
پس از پایان اجرای تست‌های کلاس
برای مثال، اگر کلاس سه متد تست داشته باشد، در اجرای معمول هر چهار متد مربوط به چرخه عمر به این صورت رفتار می‌کنند:
setUpClass() یک بار اجرا می‌شود.
setUp() سه بار اجرا می‌شود.
tearDown() سه بار اجرا می‌شود.
tearDownClass() یک بار اجرا می‌شود.
البته اگر راه‌اندازی با خطا مواجه شود یا اجرای تست‌ها به شکل غیرمعمول متوقف شود، این ترتیب ممکن است کامل نشود.
۴. مثال کاربردی با داده‌های مشترک
import unittest


class TestProducts(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.products = {
"book": 150,
"pen": 20,
"bag": 500,
}

def test_book_price(self):
self.assertEqual(self.products["book"], 150)

def test_pen_price(self):
self.assertEqual(self.products["pen"], 20)

def test_product_count(self):
self.assertEqual(len(self.products), 3)


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

در اینجا دیکشنری یک بار ساخته می‌شود و همه تست‌ها آن را می‌خوانند.
نکته مهم: داده‌های مشترک ممکن است بین تست‌ها تغییر کنند. اگر یک تست دیکشنری را تغییر دهد، تست‌های دیگر نیز می‌توانند تغییر را مشاهده کنند. بنابراین، برای داده‌های قابل‌تغییر یا تست‌های مستقل، معمولاً ساخت داده در setUp() انتخاب امن‌تری است.
همچنین tearDownClass() فقط زمانی اجرا می‌شود که راه‌اندازی کلاس با موفقیت انجام شده باشد. برای منابعی که باید حتی در صورت شکست بخشی از setUpClass() آزاد شوند، ثبت پاک‌سازی با cls.addClassCleanup() می‌تواند مناسب‌تر باشد.
🎯 تمرین
کلاسی به نام TestInventory بساز که:
۱. در setUpClass() یک دیکشنری مشترک از سه محصول ایجاد کند.
۲. در تست اول، قیمت یکی از محصولات را بررسی کند.
۳. در تست دوم، تعداد محصولات را بررسی کند.
۴. در tearDownClass() پیام پایان تست‌های کلاس را چاپ کند.
۵. توضیح بده چرا نباید بدون دلیل، داده‌های مشترک را در هر تست تغییر دهیم.
اجرای تست‌ها:
python -m unittest -v

در پست بعدی به سراغ مدیریت خطا هنگام آماده‌سازی منابع تست می‌رویم تا تست‌ها حتی در شرایط شکست نیز پاک‌سازی قابل‌اعتمادتری داشته باشند.
پست ۲۴۷ از دوره جامع Python 🐍
1👍1
🐍 آموزش جامع Python — پست ۲۴۸
پاک‌سازی مطمئن منابع با addClassCleanup()
در پست قبل با setUpClass() و tearDownClass() آشنا شدیم. حالا یک مسئله مهم را بررسی می‌کنیم: اگر آماده‌سازی منابع با خطا مواجه شود، چگونه مطمئن شویم منابعی که قبلاً ایجاد شده‌اند آزاد می‌شوند؟
برای این کار، unittest متدی به نام addClassCleanup() در اختیارمان قرار می‌دهد.
۱. مشکل tearDownClass()
فرض کنید در مرحله آماده‌سازی کلاس، یک فایل باز می‌کنیم و سپس هنگام آماده‌سازی منبع دیگری خطایی رخ می‌دهد.
import unittest


class TestResources(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.file = open(
"test.txt",
"w",
encoding="utf-8"
)

raise RuntimeError("Setup failed")

@classmethod
def tearDownClass(cls):
cls.file.close()

در این مثال، اگر setUpClass() با خطا متوقف شود، نباید به اجرای tearDownClass() برای بستن فایل تکیه کنیم. در نتیجه، فایل ممکن است باز بماند.
۲. راه‌حل با addClassCleanup()
بهتر است عملیات پاک‌سازی را بلافاصله پس از ایجاد منبع ثبت کنیم:
import unittest


class TestResources(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.file = open(
"test.txt",
"w",
encoding="utf-8"
)

cls.addClassCleanup(cls.file.close)

# ادامه آماده‌سازی منابع
# اگر این بخش خطا بدهد، پاک‌سازی ثبت‌شده اجرا می‌شود.

def test_file_is_open(self):
self.assertFalse(self.file.closed)


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

🔍 چه اتفاقی می‌افتد؟
۱. فایل باز می‌شود.
۲. تابع close با addClassCleanup() ثبت می‌شود.
۳. اگر ادامه آماده‌سازی کلاس با خطا مواجه شود، عملیات پاک‌سازی ثبت‌شده همچنان اجرا می‌شود.
۴. اگر تست‌ها اجرا شوند، پاک‌سازی کلاس پس از پایان اجرای تست‌های آن انجام می‌شود.
۳. ترتیب اجرای پاک‌سازی‌ها
اگر چند عملیات ثبت کنیم:
@classmethod
def setUpClass(cls):
cls.addClassCleanup(print, "Cleanup A")
cls.addClassCleanup(print, "Cleanup B")

عملیات پاک‌سازی به ترتیب معکوس ثبت اجرا می‌شوند؛ بنابراین ابتدا Cleanup B و سپس Cleanup A چاپ می‌شود.
این ترتیب برای منابع وابسته به یکدیگر اهمیت دارد. معمولاً باید منبعی که آخر از همه ایجاد شده، زودتر آزاد شود.
۴. تفاوت سه روش
روش
کاربرد
tearDown()
پاک‌سازی پس از هر تست
tearDownClass()
پاک‌سازی مشترک پس از اجرای تست‌های کلاس
addClassCleanup()
ثبت عملیات پاک‌سازی کلاس، حتی اگر setUpClass() شکست بخورد
نکته: addClassCleanup() در نسخه‌های جدید پایتون در دسترس است؛ برای سازگاری با نسخه‌های بسیار قدیمی، نسخه پایتون را بررسی کنید.
🎯 تمرین
کلاسی طراحی کن که در setUpClass() یک فایل باز کند و بلافاصله تابع بستن آن را با addClassCleanup() ثبت کند.
سپس:
یک تست برای بررسی باز بودن فایل بنویس.
توضیح بده چرا ثبت پاک‌سازی باید بلافاصله پس از باز کردن فایل انجام شود.
مشخص کن اگر آماده‌سازی بعد از باز شدن فایل با خطا مواجه شود، چه اتفاقی می‌افتد.
پست ۲۴۸ از دوره جامع Python 🐍
1👍1
🐍 آموزش جامع Python — پست ۲۴۹
تست‌کردن خروجی چاپ‌شده با unittest.mock.patch
گاهی تابعی مقدار مشخصی برنمی‌گرداند، بلکه نتیجه را با print() نمایش می‌دهد. در چنین شرایطی، چگونه خروجی آن را آزمایش کنیم؟
در پایتون می‌توانیم با unittest.mock.patch، جریان خروجی استاندارد یعنی sys.stdout را موقتاً جایگزین کنیم و متن چاپ‌شده را بررسی کنیم.
۱. تابعی برای نمایش اطلاعات
فرض کنید فایل reports.py شامل این تابع است:
def show_total(total):
print(f"Total: {total}")

اجرای زیر:
show_total(250)

این متن را چاپ می‌کند:
Total: 250

اما برای تست خودکار، بهتر است به‌جای بررسی دستی ترمینال، خروجی را در تست دریافت کنیم.
۲. استفاده از patch() و StringIO
import unittest
from unittest.mock import patch
from io import StringIO

from reports import show_total


class TestReports(unittest.TestCase):

@patch("sys.stdout", new_callable=StringIO)
def test_show_total(self, mock_stdout):
show_total(250)

self.assertEqual(
mock_stdout.getvalue(),
"Total: 250\n"
)


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

۳. بررسی بخش‌های مهم کد
StringIO چیست؟
StringIO یک جریان متنی در حافظه است که می‌توان روی آن نوشت و محتوایش را خواند؛ بدون اینکه فایل واقعی روی دیسک ایجاد شود.
patch("sys.stdout", new_callable=StringIO) چه می‌کند؟
در طول تست، sys.stdout را با یک نمونه جدید از StringIO جایگزین می‌کند. بنابراین، فراخوانی print() متن را در این جریان می‌نویسد.
getvalue() چه کاری انجام می‌دهد؟
تمام متن ذخیره‌شده در جریان را به‌صورت رشته برمی‌گرداند.
۴. چرا در انتهای خروجی \n داریم؟
تابع print() به‌طور پیش‌فرض در پایان متن یک کاراکتر خط جدید اضافه می‌کند.
بنابراین:
print("Python")

معمولاً خروجی ثبت‌شده زیر را دارد:
"Python\n"

اگر مقدار end را تغییر دهیم، نتیجه هم تغییر می‌کند:
print("Python", end="!")

در این حالت، خروجی دقیق برابر "Python!" خواهد بود.
۵. بررسی خروجی چندخطی
تابع زیر را در نظر بگیر:
def show_user(name, age):
print(f"Name: {name}")
print(f"Age: {age}")

تست آن می‌تواند به این شکل باشد:
@patch("sys.stdout", new_callable=StringIO)
def test_show_user(self, mock_stdout):
show_user("Sara", 25)

self.assertEqual(
mock_stdout.getvalue(),
"Name: Sara\nAge: 25\n"
)

توجه کنید که این متد تست باید داخل یک کلاس فرزند unittest.TestCase قرار بگیرد.
⚠️ نکته مهم
جایگزینی sys.stdout بر خروجی‌هایی اثر می‌گذارد که از همان جریان استفاده می‌کنند. اگر کد مستقیماً از جریان دیگری، مانند sys.stderr، استفاده کند، باید آن جریان را جداگانه بررسی کنید.
همچنین برای تست خروجی چاپ‌شده، معمولاً بهتر است متن مورد انتظار را دقیق بررسی کنید؛ فاصله‌ها و خط‌های جدید هم بخشی از خروجی هستند.
🎯 تمرین
تابعی به نام display_product(name, price) بنویس که این دو خط را چاپ کند:
Product: Keyboard
Price: 1200

سپس با patch() و StringIO تستی بنویس که خروجی کامل را بررسی کند.
پست ۲۴۹ از دوره جامع Python 🐍
1👍1
🐍 آموزش جامع Python — پست ۲۵۰
تست‌کردن فایل‌ها و پوشه‌ها با tempfile
در پروژه‌های واقعی، توابع زیادی با فایل‌ها و پوشه‌ها کار می‌کنند؛ مثلاً خواندن تنظیمات، ذخیره گزارش یا پردازش فایل‌های ورودی.
اما اگر تست‌ها مستقیماً فایل‌های واقعی پروژه را تغییر دهند، ممکن است اطلاعات مهمی از بین برود یا نتیجه تست‌ها به وضعیت سیستم وابسته شود.
ماژول استاندارد tempfile امکان ایجاد فایل‌ها و پوشه‌های موقت را فراهم می‌کند تا تست‌ها محیط مستقل‌تری داشته باشند.
۱. ایجاد فایل موقت با TemporaryFile
import tempfile

with tempfile.TemporaryFile(
mode="w+t",
encoding="utf-8"
) as file:
file.write("Hello Python")
file.seek(0)

content = file.read()
print(content)

خروجی:
Hello Python

در این مثال:
w+t فایل را برای خواندن و نوشتن متن باز می‌کند.
write() متن را می‌نویسد.
seek(0) موقعیت خواندن را به ابتدای فایل برمی‌گرداند.
read() محتوای فایل را می‌خواند.
با پایان بلوک with، فایل بسته می‌شود و فایل موقت معمولاً به‌صورت خودکار حذف می‌شود.
رفتار دقیق حذف فایل موقت به نوع تابع و سیستم‌عامل وابسته است.
۲. تست یک تابع که از مسیر فایل استفاده می‌کند
فرض کنید تابع زیر در فایل config_reader.py قرار دارد:
from pathlib import Path


def read_config(path):
return Path(path).read_text(encoding="utf-8")

می‌خواهیم بررسی کنیم که تابع محتوای فایل را درست می‌خواند؛ بدون اینکه به یک فایل تنظیمات واقعی وابسته باشیم.
نوشتن تست
import unittest
import tempfile
from pathlib import Path

from config_reader import read_config


class TestConfigReader(unittest.TestCase):

def test_read_config(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "config.txt"
path.write_text(
"theme=dark",
encoding="utf-8"
)

result = read_config(path)

self.assertEqual(result, "theme=dark")


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

🔍 چرا این روش مناسب است؟
۱. TemporaryDirectory() یک پوشه موقت ایجاد می‌کند.
۲. فایل config.txt داخل همان پوشه ساخته می‌شود.
۳. تابع واقعی read_config() روی مسیر فایل اجرا می‌شود.
۴. با پایان بلوک with، پوشه موقت و محتوای آن برای پاک‌سازی علامت‌گذاری و حذف می‌شوند.
در این تست از patch() استفاده نکردیم؛ چون هدف ما بررسی رفتار واقعی خواندن فایل است، نه شبیه‌سازی آن.
۳. تفاوت ابزارهای مهم tempfile
ابزار
کاربرد
TemporaryFile()
ایجاد فایل موقت برای کار مستقیم با محتوای آن
NamedTemporaryFile()
ایجاد فایل موقت با نام و مسیر قابل‌دسترسی
TemporaryDirectory()
ایجاد پوشه موقت برای نگهداری یک یا چند فایل
SpooledTemporaryFile()
نگهداری داده در حافظه تا رسیدن به آستانه مشخص و سپس انتقال به فایل موقت
نکته: اگر تابع مورد آزمایش به مسیر واقعی فایل نیاز دارد، TemporaryDirectory() یا NamedTemporaryFile() معمولاً انتخاب مناسب‌تری از TemporaryFile() است؛ زیرا TemporaryFile() الزاماً مسیر قابل‌استفاده‌ای در اختیار برنامه قرار نمی‌دهد.
۴. یک اشتباه رایج
اگر تابع شما مسیر نسبی می‌گیرد، نباید فرض کنید فایل همیشه نسبت به پوشه پروژه پیدا می‌شود. مسیر نسبی معمولاً نسبت به پوشه کاری فعلی فرایند تفسیر می‌شود.
استفاده از مسیر صریح ساخته‌شده با Path در تست، وابستگی به پوشه کاری را کاهش می‌دهد.
🎯 تمرین
تابعی به نام count_lines(path) بنویس که تعداد خطوط یک فایل متنی را برگرداند.
سپس با TemporaryDirectory() فایلی بساز که شامل سه خط باشد و تستی بنویس که نتیجه تابع برابر 3 باشد.
پست ۲۵۰ از دوره جامع Python 🐍
1👍1
🐍 آموزش جامع Python — پست ۲۵۱
تست‌کردن کدهای ناهمگام با unittest.IsolatedAsyncioTestCase
در پایتون، توابعی که با async def تعریف می‌شوند، توابع ناهمگام (Asynchronous) هستند. برای تست این توابع باید اجرای آن‌ها را با await مدیریت کنیم.
کتابخانه استاندارد unittest کلاسی به نام IsolatedAsyncioTestCase دارد که برای تست کدهای ناهمگام طراحی شده است.
🔹 ۱. یک تابع ناهمگام بسازیم
import asyncio


async def get_user_name():
await asyncio.sleep(0.1)
return "Sara"

فراخوانی این تابع یک شیء coroutine ایجاد می‌کند. برای دریافت نتیجه، باید آن را اجرا و منتظر تکمیلش بمانیم.
🔹 ۲. نوشتن تست ناهمگام
import unittest
import asyncio


async def get_user_name():
await asyncio.sleep(0.1)
return "Sara"


class TestUser(unittest.IsolatedAsyncioTestCase):

async def test_get_user_name(self):
result = await get_user_name()

self.assertEqual(result, "Sara")


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

🔍 نکات مهم
متد تست ناهمگام باید با async def تعریف شود.
درون متد تست، برای انتظار نتیجه تابع ناهمگام از await استفاده می‌کنیم.
IsolatedAsyncioTestCase محیط اجرای ناهمگام مستقلی برای تست‌ها فراهم می‌کند.
در این مثال، asyncio.sleep() صرفاً برای شبیه‌سازی یک عملیات زمان‌بر است.
🔹 ۳. بررسی خطا در تابع ناهمگام
می‌توانیم خطاهای مورد انتظار را نیز آزمایش کنیم:
async def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")

return a / b

تست:
async def test_divide_by_zero(self):
with self.assertRaises(ValueError):
await divide(10, 0)

این متد تست باید داخل کلاس TestUser یا کلاس دیگری از نوع IsolatedAsyncioTestCase قرار بگیرد.
نکته: اگر از assertRaises() استفاده می‌کنیم، باید فراخوانی coroutine را با await داخل بلوک آن قرار دهیم تا خطای زمان اجرای تابع واقعاً بررسی شود.
🎯 تمرین
تابع ناهمگام زیر را پیاده‌سازی و آزمایش کن:
async def get_square(number)
این تابع باید مربع عدد را برگرداند. سپس دو تست بنویس:
۱. بررسی کن که get_square(5) نتیجه 25 را برمی‌گرداند.
۲. اگر تصمیم گرفتی ورودی منفی مجاز نباشد، برای آن ValueError تعریف کن و تست مناسبی بنویس.
اجرای تست‌ها:
python -m unittest -v

پست ۲۵۱ از دوره جامع Python 🐍
👍1
🐍 آموزش جامع Python — پست ۲۵۲
شبیه‌سازی توابع ناهمگام با AsyncMock
در پست قبل یاد گرفتیم توابع async را با IsolatedAsyncioTestCase آزمایش کنیم. حالا می‌خواهیم وابستگی‌های ناهمگام را بدون اجرای عملیات واقعی شبیه‌سازی کنیم.
ابزار AsyncMock از ماژول unittest.mock برای همین کار طراحی شده است.
🔹 ۱. چرا به AsyncMock نیاز داریم؟
فرض کنید تابعی برای دریافت اطلاعات کاربر از یک API داریم. هنگام تست نمی‌خواهیم درخواست واقعی اینترنتی ارسال کنیم؛ فقط می‌خواهیم رفتار تابع خودمان را بررسی کنیم.
فایل user_service.py:
async def fetch_user(api):
return await api.get_user(10)

این تابع به یک متد ناهمگام به نام get_user() وابسته است.
🔹 ۲. نوشتن تست با AsyncMock
import unittest
from unittest.mock import AsyncMock


async def fetch_user(api):
return await api.get_user(10)


class TestUserService(unittest.IsolatedAsyncioTestCase):

async def test_fetch_user(self):
api = unittest.mock.Mock()
api.get_user = AsyncMock(
return_value={"id": 10, "name": "Sara"}
)

result = await fetch_user(api)

self.assertEqual(
result,
{"id": 10, "name": "Sara"}
)

api.get_user.assert_awaited_once_with(10)


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

🔍 توضیح کد
AsyncMock یک وابستگی ناهمگام ساختگی ایجاد می‌کند.
return_value مشخص می‌کند که انتظار داریم نتیجه await api.get_user(10) چه باشد.
assert_awaited_once_with(10) بررسی می‌کند که متد دقیقاً یک بار با آرگومان 10 مورد await قرار گرفته باشد.
در این تست، هیچ درخواست واقعی به API ارسال نمی‌شود.
🔸 تفاوت assert_called_once_with() و assert_awaited_once_with()
متد
چه چیزی را بررسی می‌کند؟
assert_called_once_with()
تابع یک بار با آرگومان‌های مشخص فراخوانی شده است
assert_awaited_once_with()
فراخوانی ناهمگام یک بار با آرگومان‌های مشخص مورد await قرار گرفته است
برای بررسی رفتار ناهمگام، معمولاً assert_awaited_once_with() دقیق‌تر است.
⚠️ نکته مهم
در کد بالا از unittest.mock.Mock() استفاده کرده‌ایم؛ بنابراین باید import unittest نیز وجود داشته باشد. اگر فقط AsyncMock را وارد کرده باشید، می‌توانید به‌جای آن مستقیماً بنویسید:
from unittest.mock import AsyncMock, Mock

api = Mock()
api.get_user = AsyncMock(
return_value={"id": 10, "name": "Sara"}
)

🎯 تمرین
تابعی به نام fetch_product(api) بنویس که محصول شماره 5 را با await api.get_product(5) دریافت کند.
سپس با AsyncMock تستی بنویس که:
۱. نتیجه ساختگی {"id": 5, "name": "Keyboard"} باشد.
۲. نتیجه تابع را بررسی کند.
۳. تأیید کند get_product دقیقاً یک بار با آرگومان 5 مورد await قرار گرفته است.
پست ۲۵۲ از دوره جامع Python 🐍
👍1
🐍 آموزش جامع Python — پست ۲۵۳
تست‌کردن فراخوانی‌ها با call_args و call_args_list
در تست‌نویسی، گاهی فقط دانستن نتیجه یک تابع کافی نیست؛ باید بررسی کنیم تابع‌های وابسته با چه آرگومان‌هایی و چند بار فراخوانی شده‌اند.
در unittest.mock ابزارهایی مانند call_args و call_args_list این امکان را فراهم می‌کنند.
🔹 ۱. بررسی آخرین فراخوانی با call_args
from unittest.mock import Mock

send_message = Mock()

send_message("Sara", "Hello")
send_message("Ali", "Welcome")

print(send_message.call_args)

خروجی:
call('Ali', 'Welcome')

ویژگی call_args اطلاعات آخرین فراخوانی را نگه می‌دارد؛ نه تمام فراخوانی‌ها را.
🔹 ۲. بررسی تمام فراخوانی‌ها با call_args_list
from unittest.mock import Mock

send_message = Mock()

send_message("Sara", "Hello")
send_message("Ali", "Welcome")

print(send_message.call_args_list)

خروجی:
[call('Sara', 'Hello'), call('Ali', 'Welcome')]

این ویژگی تمام فراخوانی‌ها را به ترتیب وقوع نگه می‌دارد.
🔹 ۳. بررسی آرگومان‌ها
هر فراخوانی ثبت‌شده شامل دو بخش است:
args: آرگومان‌های موقعیتی (Positional Arguments)
kwargs: آرگومان‌های نام‌دار (Keyword Arguments)
مثال:
from unittest.mock import Mock

save_user = Mock()

save_user(10, name="Sara", active=True)

args, kwargs = save_user.call_args

print(args)
print(kwargs)

خروجی:
(10,)
{'name': 'Sara', 'active': True}

وجود ویرگول در (10,) نشان می‌دهد که این یک تاپل تک‌عضوی است.
🔹 ۴. استفاده در تست واقعی
فرض کنیم تابع زیر را داریم:
def notify_users(send_message, users):
for user in users:
send_message(user, "New update")

تست آن:
import unittest
from unittest.mock import Mock, call


class TestNotifications(unittest.TestCase):

def test_notify_users(self):
send_message = Mock()

notify_users(
send_message,
["Sara", "Ali"]
)

self.assertEqual(
send_message.call_args_list,
[
call("Sara", "New update"),
call("Ali", "New update"),
],
)


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

در این تست، علاوه بر تعداد فراخوانی‌ها، ترتیب و آرگومان‌های هر فراخوانی نیز بررسی می‌شود.
⚠️ نکته مهم
اگر فقط می‌خواهید تعداد فراخوانی‌ها را بررسی کنید، از call_count استفاده کنید. اگر آخرین آرگومان‌ها مهم‌اند، call_args مناسب است؛ و اگر تمام فراخوانی‌ها اهمیت دارند، call_args_list انتخاب بهتری است.
همچنین assert_has_calls() برای بررسی وجود یک توالی از فراخوانی‌ها کاربرد دارد؛ به‌طور پیش‌فرض فراخوانی‌های اضافی قبل یا بعد از آن توالی مجازند، اما ترتیب فراخوانی‌های مورد انتظار باید حفظ شود.
🎯 تمرین
تابعی به نام log_actions(logger, actions) بنویس که برای هر عضو فهرست، تابع logger را با آن عضو فراخوانی کند.
سپس با Mock بررسی کن:
۱. تعداد فراخوانی‌ها برابر تعداد اعضای فهرست باشد.
۲. آرگومان‌های هر فراخوانی درست باشند.
۳. ترتیب فراخوانی‌ها مطابق ترتیب فهرست ورودی باشد.
پست ۲۵۳ از دوره جامع Python 🐍
👍1
🐍 آموزش جامع Python — پست ۲۵۴
تست‌کردن فراخوانی‌های زنجیره‌ای با mock_calls
در پست قبل با call_args و call_args_list آشنا شدیم. حالا می‌خواهیم حالتی را بررسی کنیم که یک شیء ساختگی، متدهای تو‌در‌تو یا زنجیره‌ای دارد.
برای بررسی تمام این فراخوانی‌ها می‌توانیم از ویژگی mock_calls استفاده کنیم.
🔹 ۱. یک مثال ساده
from unittest.mock import Mock

client = Mock()

client.session.get("/users")
client.session.close()

print(client.mock_calls)

خروجی:
[call.session.get('/users'), call.session.close()]

ویژگی mock_calls فراخوانی‌های ثبت‌شده روی خود Mock و متدها و ویژگی‌های ساختگی وابسته به آن را به ترتیب وقوع نگه می‌دارد.
🔹 ۲. تفاوت call_args_list و mock_calls
from unittest.mock import Mock

client = Mock()

client.get("/users")
client.session.close()

print(client.call_args_list)
print(client.mock_calls)

خروجی:
[call('/users')]
[call.get('/users'), call.session.close()]

در این مثال:
call_args_list فقط فراخوانی‌های مستقیم خود client را ثبت می‌کند.
mock_calls فراخوانی‌های متدها و اشیای ساختگی وابسته را نیز ثبت می‌کند.
🔹 ۳. استفاده در تست
فرض کنیم تابعی داریم که اطلاعات را دریافت و سپس اتصال را می‌بندد:
def load_users(client):
client.session.get("/users")
client.session.close()

تست:
import unittest
from unittest.mock import Mock, call


class TestUserClient(unittest.TestCase):

def test_load_users(self):
client = Mock()

load_users(client)

self.assertEqual(
client.mock_calls,
[
call.session.get("/users"),
call.session.close(),
],
)


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

این تست بررسی می‌کند که هر دو فراخوانی با آرگومان‌های درست و به ترتیب مورد انتظار اتفاق افتاده‌اند.
⚠️ نکته مهم
mock_calls ترتیب فراخوانی‌ها را نشان می‌دهد؛ اما همیشه برای اثبات ترتیب اجرای تمام عملیات داخلی کافی نیست. به‌خصوص، آرگومان‌های یک فراخوانی والد در اطلاعات فراخوانی‌های تو‌در‌توی آن تکرار نمی‌شوند. اگر بررسی دقیق آرگومان‌های والد ضروری است، آن‌ها را جداگانه آزمایش کنید.
همچنین استفاده از Mock نباید جای تست رفتار واقعی سیستم را بگیرد؛ Mock برای بررسی تعامل با وابستگی‌ها مناسب است.
🎯 تمرین
تابعی به نام process_order(api) بنویس که به‌ترتیب:
۱. api.orders.create() را با مقدار "ORD-101" فراخوانی کند.
۲. api.orders.confirm() را فراخوانی کند.
سپس با Mock و mock_calls بررسی کن که این دو عملیات به ترتیب درست انجام شده‌اند.
پست ۲۵۴ از دوره جامع Python 🐍
👍1