آموزش

آموزش یونیت تست در پایتون

اینجا یاد می‌گیری یونیت تست چیه، چرا اهمیت داره و چطور می‌تونی اون رو با استفاده از پایتون پیاده‌سازی کنی.

تاریخ انتشار:
20 مرداد 1405
پایتون
10 دقیقه
کاربرهای فینکا در چه شرکت‌هایی مشغول به کار هستند؟

یونیت تست (Unit Test) یکی از روش‌های تست نرم‌افزاره که در اون، هر بخش مستقل کد به‌صورت جداگانه آزمایش می‌شه تا مشخص بشه درست کار می‌کنه و آماده استفاده هست یا نه. این کار به ارزیابی کیفیت کد خیلی کمک می‌کنه.

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

انتظار می‌ره توسعه‌دهنده‌ها اسکریپت‌های تست خودکار (Automated Tests) بنویسن تا خیالشون راحت بشه که هر بخش از برنامه دقیقاً همون‌طوری که طراحی شده کار می‌کنه و رفتار درستی داره.

یونیت تست در پایتون

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

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

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

هر «واحد» در تست‌ها می‌تونه یکی از موارد زیر باشه:

  • یه ماژول کامل،
  • یه تابع مجزا،
  • یه رابط کامل مثل یه کلاس یا یه متد.

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

فریم‌ورک unittest پایتون از JUnit جاوا الهام گرفته شده و امکاناتش شبیه فریم‌ورک‌های معروف زبان‌های دیگه‌ست. این فریم‌ورک ویژگی‌های مختلفی داره؛ مثل:

  • خودکارسازی تست‌ها
  • به‌اشتراک‌گذاری کدهای آماده‌سازی و پاک‌سازی برای تست‌ها
  • گروه‌بندی تست‌ها در مجموعه‌های مختلف
  • مستقل بودن تست‌ها از فریم‌ورک گزارش‌دهی

حالا بیا با هم یه مثال رو بررسی کنیم تا بهتر متوجه بشی چرا به یونیت تست برای کدهات نیاز داری.

شروع کار با unittest در پایتون

در ادامه، مراحلی که برای استفاده از فریم‌ورک unittest پایتون لازم داری رو قدم‌به‌قدم آوردیم.

نوشتن تابع محاسبه حجم

بیا اول یه کد پایتون بنویسیم که حجم یه مکعب مستطیل رو برامون حساب کنه.

خروجی
The volume of cuboid: 8
The volume of cuboid: 1.3310000000000004
The volume of cuboid: -15.625
The volume of cuboid: (-0-8j)
Traceback (most recent call last):
  File "c:\Users\Finca\Desktop\test\volume_cuboid.py", line 7, in <module>
    print ("The volume of cuboid:",cuboid_volume(length[i]))
                                   ~~~~~~~~~~~~~^^^^^^^^^^^
  File "c:\Users\Finca\Desktop\test\volume_cuboid.py", line 2, in cuboid_volume
    return (l*l*l)
            ~^~
TypeError: can't multiply sequence by non-int of type 'str'

تحلیل نتیجه

خروجی بالا باید دید خوبی در مورد اهمیت یونیت تست بهت داده باشه. در کد بالا، سه تا مشکل اساسی وجود داره:

  • اول اینکه حجم مکعب مستطیل منفی شده،
  • دوم اینکه حجم به شکل یه عدد مختلط دراومده،
  • و در نهایت، به خطای TypeError خوردیم، چون نمی‌شه رشته‌ها رو در هم ضرب کرد.

خوشبختانه مشکل سوم باعث ایجاد خطا شد و اجرای برنامه رو متوقف کرد؛ اما با وجود اینکه حجم نمی‌تونه منفی یا عدد مختلط باشه، کدهای مربوط به مشکلات اول و دوم بدون هیچ خطایی اجرا شدن!

معمولاً تست‌های واحد در یه فایل جداگانه نوشته می‌شن. برای اسم‌گذاری این فایل‌ها می‌تونی از الگوهای مختلفی استفاده کنی؛ مثلاً کلمه test رو با یه آندرلاین (_) به قبل یا بعد اسم فایل اصلیت اضافه کنی.

مثلاً اگه اسم فایلی که کد اصلیت داخلش هست cuboid_volume.py باشه، اسم فایل تستت می‌تونه test_cuboid_volume.py باشه.

حالا بیا برای همون کد بالا یه یونیت تست بنویسیم.

استفاده از assertAlmostEqual

اول بیا یه فایل پایتون به اسم volume_cuboid.py بسازیم و کد محاسبه حجم رو داخلش قرار بدیم. بعدش یه فایل دیگه به اسم test_volume_cuboid.py برای نوشتن تست‌ها می‌سازیم.

در فایل تست، یه کلاس به اسم TestCuboid تعریف می‌کنیم که از ماژول unittest ارث‌بری می‌کنه. داخل این کلاس، متدهای مختلفی رو برای بررسی تابع cuboid_volume می‌نویسیم.

اولین تابعی که می‌نویسیم test_volume هست. کار این تابع اینه که چک کنه آیا خروجی تابع cuboid_volume همون چیزی هست که انتظار داریم یا نه. برای این کار، از متد assertAlmostEqual استفاده می‌کنیم.

تست ۱

حالا بیا اسکریپت بالا رو اجرا کنیم. وقتی می‌خوای ماژول unittest رو مستقیماً از طریق خط فرمان اجرا کنی، باید از فلگ -m استفاده کنی.

خروجی
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

عالیه! اولین کد یونیت تست خودت رو با موفقیت اجرا کردی.

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

تست ۲

حالا بیا ببینیم اگه یکی از بررسی‌های assertAlmostEqual به مشکل بخوره (Fail بشه)، چه اتفاقی می‌افته.

همون‌طور که می‌بینی، آخرین دستور assert رو در این کد تغییر دادیم.

خروجی
F
======================================================================
FAIL: test_volume (test_volume_cuboid.TestCuboid.test_volume)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\Finca\Desktop\test\test_volume_cuboid.py", line 9, in test_volume
    self.assertAlmostEqual(cuboid_volume(5.5), 0)
    ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 166.375 != 0 within 7 places (166.375 difference)

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

همون‌طور که در خروجی می‌بینی، آخرین assert باعث ایجاد خطای AssertionError شد و تست Fail شد. ماژول unittest خیلی راحت دلیل این شکست و تعداد خطاهایی که در طول تست پیش اومده رو بهت گزارش می‌ده.

استفاده از assertRaises

حالا بیا بریم سراغ یه متد دیگه به اسم assertRaises. این متد کمک می‌کنه بررسی کنیم که آیا تابع ما در مدیریت ورودی‌های اشتباه درست عمل می‌کنه یا نه.

مثلاً می‌خوایم مطمئن بشیم وقتی نوع داده اشتباهی به تابع cuboid_volume می‌دیم، واکنش درستی نشون می‌ده. فرض کن یه رشته به عنوان طول مکعب پاس داده بشه؛ چون طول مکعب نمی‌تونه رشته باشه، تابع باید این موضوع رو با یه Exception یا یه شرط if مدیریت کنه. با assertRaises می‌تونیم تست کنیم که آیا خطای مدنظرمون رخ می‌ده یا نه.

خروجی
F.
======================================================================
FAIL: test_input_value (test_volume_cuboid.TestCuboid.test_input_value)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\Finca\Desktop\test\test_volume_cuboid.py", line 11, in test_input_value
    self.assertRaises(TypeError, cuboid_volume, True)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: TypeError not raised by cuboid_volume

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

خب! از خروجی بالا کاملاً مشخصه که کد ما در فایل volume_cuboid.py در برابر ورودی‌های نامعتبر هیچ مکانیزم کنترلی و مدیریتی‌ای نداره.

اصلاح تابع محاسبه حجم

بیا یه شرط به volume_cuboid.py اضافه کنیم تا چک کنه آیا طول مکعب یه عدد معتبره یا نه؛ و مثلاً اگه ورودی بولین یا رشته بود، یه استثناء (Exception) تولید کنه.

خروجی
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK

اجرای اسکریپت تست از داخل فایل

اگه بخوای فایل تست رو به صورت مستقل و مستقیم اجرا کنی، می‌تونی در انتهای کدت تابع unittest.main() رو صدا بزنی. با این کار، هر بار که فایل پایتون رو ران کنی، تمام تست‌های داخلش به صورت خودکار اجرا می‌شن و نتیجه‌شون رو می‌بینی.

وقتی از unittest.main() استفاده می‌کنی، می‌تونی با آرگومان verbosity سطح جزئیات خروجی تست رو مشخص کنی.

بیا مقادیر مختلف verbosity رو امتحان کنیم و ببینیم خروجی ترمینال چه تغییری می‌کنه.

سطح جزئیات ۰

خروجی
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

سطح جزئیات ۱

خروجی
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

سطح جزئیات ۲

خروجی
test_input_value (__main__.TestCuboid.test_input_value) ... ok
test_volume (__main__.TestCuboid.test_volume) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

نحوه نمایش خطای AssertionError

برای بخش آخر، بیا آخرین متد assertAlmostEqual رو در متد test_volume تغییر بدیم تا عمداً Fail بشه و ببینیم در سطح جزئیات ۲ (verbosity=2)، خطاها چطوری نمایش داده می‌شن.

خروجی
test_input_value (__main__.TestCuboid.test_input_value) ... ok
test_volume (__main__.TestCuboid.test_volume) ... FAIL

======================================================================
FAIL: test_volume (__main__.TestCuboid.test_volume)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "c:\Users\Finca\Desktop\test\test_volume_cuboid.py", line 7, in test_volume
    self.assertAlmostEqual(cuboid_volume(0),1)
    ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
AssertionError: 0 != 1 within 7 places (1 difference)

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

خلاصه

با توجه به خروجی‌های بالا، می‌تونیم این‌طوری نتیجه‌گیری کنیم:

  • سطح جزئیات صفر (0) فقط تعداد تست‌های اجراشده و زمان اون‌ها رو نشون می‌ده.
  • سطح جزئیات یک (1) برای هر تست موفق یه نقطه (.) چاپ می‌کنه.
  • سطح جزئیات دو (2) اسم دقیق هر متد تست و وضعیت نهاییش رو (مثل OK یا FAIL) با جزئیات کامل نمایش می‌ده.

متدهای Assert در unittest

ماژول unittest در پایتون متدهای assert متنوعی داره که بسته به نیازت در تست‌ها می‌تونی ازشون استفاده کنی.

متدهای پرکاربرد assert

متد بررسی می‌کنه که...
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)

تست کردن استثناها، هشدارها و لاگ‌ها

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

متد بررسی می‌کنه که...
assertRaises(exc, fun, *args, **kwds) اجرای fun(*args, **kwds) خطای exc رو ایجاد می‌کنه.
assertRaisesRegex(exc, r, fun, *args, **kwds) اجرای fun(*args, **kwds) خطای exc رو ایجاد می‌کنه و پیام خطا با رجکس r مطابقت داره.
assertWarns(warn, fun, *args, **kwds) اجرای fun(*args, **kwds) هشدار warn رو ایجاد می‌کنه.
assertWarnsRegex(warn, r, fun, *args, **kwds) اجرای fun(*args, **kwds) هشدار warn رو ایجاد می‌کنه و پیام هشدار با رجکس r مطابقت داره.
assertLogs(logger, level) کدهای داخل with، در logger پیامی با حداقل سطح level ثبت می‌کنن.
assertNoLogs(logger, level) کدهای داخل with، در logger هیچ پیامی با سطح level یا بالاتر ثبت نمی‌کنن.

متدهای مخصوص وظایف خاص

متد بررسی می‌کنه که...
assertAlmostEqual(a, b) round(a-b, 7) == 0
assertNotAlmostEqual(a, b) round(a-b, 7) != 0
assertGreater(a, b) a > b
assertGreaterEqual(a, b) a >= b
assertLess(a, b) a < b
assertLessEqual(a, b) a <= b
assertRegex(s, r) رجکس r در رشته s پیدا می‌شه (r.search(s)).
assertNotRegex(s, r) رجکس r در رشته s پیدا نمی‌شه (not r.search(s)).
assertCountEqual(a, b) a و b، بدون توجه به ترتیب، دارای عناصر یکسان با تعداد تکرار یکسان هستن.

برای اینکه دقیق‌تر با متدهای assert آشنا بشی، بهتره به مستندات رسمی پایتون سر بزنی.

علاوه بر این، می‌تونی از ماژول Pydoc (که شبیه تابع help در پایتونه) استفاده کنی تا مستندات هر متد رو مستقیماً بخونی. در ادامه یه مثال از نحوه استفاده از Pydoc برای دیدن مستندات متد assertCountEqual آوردیم.

جمع‌بندی

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

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

اشتراک‌گذاری
فهرست مطالب
  • یونیت تست در پایتون
  • شروع کار با unittest در پایتون
  • اجرای اسکریپت تست از داخل فایل
  • متدهای Assert در unittest
  • جمع‌بندی

دریافت اپلیکیشن فینکا

با اپلیکیشن فینکا، به بیشتر دوره‌ها و مسیرها روی موبایل دسترسی دارید، تمرین می‌کنید و یادگیری رو هم‌زمان روی موبایل و دسکتاپ ادامه می‌دید.