جلسه هفتگی آموزش تیم بود و امیر افیانیان درمورد pytest مطلبی رو ارائه داد. از اونجایی هم که حدود چهار یا پنچ ماه پیش یک مطلب ناقصی در رابطه با این موضوع نوشته بودم، ترغیب شدم کاملش کنم تا این به اشتراکگذاری دانش ادامه پیدا کنه. :)
خب بریم سراغ pytest و ببینیم که دلیل محبوبیت این فریمورک چیه و چرا این روزها ازش زیاد استفاده میشه؟ تو این مطلب سعی میکنیم با بعضی از ویژگیهای این فریم ورک آشنا بشیم تا ما هم بریم تو گروه اونایی که از این فریم ورک برای تست نویسی استفاده میکنند!!
بیاین با اولین جملهای که تو داکیومنت این فریمورک اومده شروع کنیم:
The pytest
framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries.
یعنی دو خط تست بنویسی و تامام؟ بریم ببنیم چطوریاست که این طوریاست.(علیرضا دو خط!)
بدلیل اینکه مطلب مربوط به نوشتن تستهای باکیفیت برای فهم بهتر این مطلب می تونه کمک کننده باشه، و گاها هم بهش اشارههایی میشه به همین دلیل لینکش رو میزارم.
نکته: طبیعتا امکان گفتن همه ویژگیهای pytest وجود نداره(به دلیل سواد ناقص من) و فقط مواردی که احتمالا بیشتر مفید هستد(و من باهاشون آشنا هستم)، گفته می شوند.
این مورد ویژگی خاصی نیست! ولی خب باعث راحتی کار میشه. دیگه موقع تستنویسی نیازی نیست که نوع برابری رو هم مشخص کنیم. یعنی assertDict یا assertTrue و ... را نمیخواد و فقط با نوشتن assert میتونیم مقدار مورد انتظارمون رو با مقدار تست شده بررسی کنیم.
به عنوان مثال تو unit test داشتیم:
from unittest import TestCase class TestUserInput(TestCase): def test_input_values_validation_true(self): # stuff related to create input_values self.assertTrue(validate_values(input_values))
حالا اگر بخوایم این مورد را با pytest بنویسیم خواهیم داشت:
def test_input_values_validation_True(): # stuff related to create input_values assert validate_values(input_values) == True
اگر به این دو تیکه کد دقیقتر نگاه کنیم به یکی دیگه از ویژگیهای pytest میرسیم که اونم میشه:
اگر توجه کرده باشین تو تکه کد اول ما باید اول import unittest کنیم و بعد کلاس رو بنویسم و بعدش تازه میتونیم تست خودمون رو بنویسم ولی توی pytest حتی نیاز به import کردن هم نیست!! این مورد بعدا تو parameterize هم خودش رو نشون میده.
یکی از دلایلی که مهاجرت به pytest رو راحت و کم هزینهتر میکنه اینکه اگر قبلا تستهایی رو با unitest و یا nose نوشته باشیم، بدون هیچ نگرانی بابت اونها شروع به نوشتن تستهای جدید با pytest میکنیم(واقعا امتیاز مهمیه).
بعضی موارد توی کد هستند که باید با شرایط مختلف تست بشوند. مثلا با ورودی های مختلفی مثل عدد منفی، مثبت، رشته و ... . نکتهی اذیت کننده اینجاست که برای هر حالتی باید یک تست جداگانه نوشته بشه، که خیلی هم بهم دیگه شبیه هستند و این واقعا رو مخه. pytest این مورد به زیبایی و با parameterize حلش کرده. به عنوان مثال:
from datetime import datetime, timedelta import pytest # یک نکته ریز # کردن ماژولها، اونهایی که برای خود پایتون هستند اول میان import موقع # نصب شدن و آخر سر هم ماژولهای خودمون pip بعد اون پکیچ هایی که با testdata = [ (datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)), (datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)), ] @pytest.mark.parametrize("a,b,expected", testdata) def test_timedistance(a, b, expected): diff = a - b assert diff == expected`
تو کد بالا ما دو حالت (یا هر چندتا که دلمون بخواد) را به راحتی تست کردیم و چی از این بهتر؟
همون طور که قبلا گفتیم استفاده از توابع setUp و tearDown باعث ناخوانایی کد میشه( اگر نمیدونین چرا یه به مطلب تست های باکیفیت سر بزنین. به صورت خلاصه چون به صورت ضمنی در هر تست وجود دارند). اما pytest برای فراهم کردن دادههای موردنیاز از fixtureها استفاده میکنه.fixture علاوه بر اینکه به صورت صریح به تابع تست (کلاس تست یا کل حتی ماژول) پاس داده میشه(در واقع خیلی خوشگل و زیبا داره برامون dependency injection میکنه) و باعث خوانایی کد میشه. البته یک سری ویژگی دیگه هم داره، مثلا میشه با parameterize ترکیبش کرد تا کارمون راحتتر بشه و البته خیلی چیزهای دیگه. fixture با درخواست تست مرتبطش ایجاد و با تموم شدن scopeش از بین میره.
برای scope هم پنج حالت داریم که عبارتاند از:
− function(پیشفرض)
− class
− module
− package
− session
البته scope داینامیک هم داریم!
بحث رو طولانیتر نکنیم و بریم سراغ مثال و کد:
import pytest from .my_module import TargetClass @pytest.fixture def target_class_instance(): return TargetClass("a input value") def test_validation_input(target_class_instance): assert target_class_instance.validate() == True
از اونجایی که هدف این مطلب آشنایی با pytest هستش، به خاطر همین بیشتر از این به fixture نمیپردازیم. اما واقعا شاخه و برگ زیادی داره و حتی میشه یک یا چندتا مطلب برای fixture نوشت.توصیه میکنم به صورت تیتروار هم که شده این لینک رو مطالعه کنید.
این ویژگی باعث میشه که به تستهامون مِتادیتا اضافه کنیم و براساس این مِتادیتا اتفاقاتی موقع اجرای تستها بیوفته.
به عنوان فرض کنید در لحظه بنا به بعضی از شرایط و اتفاقات لازم داریم که یک سری از تست ها skip بشن. خیلی راحت به اون تستها mark مربوط یعنی skip رو میزنیم تا موقع موقع اجرا، این تستها در نظر گرفته نشوند. یا یک سری تست باید در صورت برقراری برخی شرایط اجرا بشن(مثلا بر اساس نسخه پایتون). میایم از skipif استفاده میکنیم.
import sys import pytest @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher") def test_function(): ...
یکی از قابلیت های خیلی خوب pytest تنظیمات بینظیرشه که باعث میشه سرعت کار ادم بیشتر بشه. این تنظیمات و فلگهای مختلفی که برای اجرای تستها وجود داره، دست آدم رو باز و لذت کار رو بیشتر میکند. البته باعث تسریع کل فرآیند هم میشه، که این خودش در نهایت باعث کمک کردن به اصل fast عمو باب میکنه. به عنوان نمونه فقط به چند مورد اشاره میکنیم(دوست داشتین خودتون هم یک نگاه به داکیومنتها بندازین).
− امکان اجرای تستهایی که شامل یک سری رشته خاص هستند.
− امکان توقف اجرای پروسهی تست بعد از چند(N) تست fail شده.
− امکان مشخص کردن میزان verbosity خروجی
− امکان اجرای خودکار pdb بعد از fail شدن تست و ...
علاوه بر اینکه همه این ها امکان پاس دادن مستقیم از طریق command line رو دارند، میتونیم کانفیگ فایل خودمون رو هم داشته باشیم. خوبیه این کار چیه؟ به عنوان مثال ما همیشه موقع اجرای تستها اونهارو با یک سری فلگ مشخص اجرا میکنیم اما از اینکه هر بار این فلگها رو بزنیم شاکیایم و غر میزنیم. خیلی راحت فایل رو ایجاد میکنیم، تا موقع اجرا فلگهای مدنظرمون به صورت خودکار اضافه بشوند.
# pytest.ini [pytest] minversion = 6.0 addopts = -ra -q testpaths = tests integration
یکی از مواردی که باعث دوست داشتنیتر شدن این فریمورک میشه وجود extensionهای کاربردیه اونه. به عنوان مثال pytest-mock یا pytest-cov که اکثرا مورد استفاده قرار میگیرند. یک استفاده خوبی که از ویژگی میشه داشت اینکه براساس نیاز خودمون extension بنویسیم. مثلا یک چیزی بنویسیم که تو CI نتیجه اجرای تستهامون رو تو سیستم tracking ثبت کنه(چقدر طول کشیده، fail یا warning داشتیم یا نه و ...) .
چند مورد از extensionها که ممکنه به کارمون بیان:
pytest-mock, pytest-cov, pytest-django, pytest-flask, pytest-xdist
اگر نکتهای هست که به نظرتون فراموش شده یا سوالی، موردی، یا کلا دوست داشتین یک چیزی بگین، خوشحال میشم تو کامنتها بگین.
همین. لبخند بزنین لطفا :)
منابع