تست کدهای پایتون توسط pytest
۳۱ تیر ۱۳۹۹
تستگرفتن از کدهایتان در یک پروژه، مزایای بسیاری دارد. این قضیه باعث افزایش اعتماد به نفس شما میشود، زیرا که کدتان همانطوری که انتظارش را داشتهاید، رفتار کرده است و این اطمینان را میدهد که تغییرها کدتان باعث بازگشت به عقب نمیشود. نوشتن و مدیریت تستها برای کدتان کار سختی است، بنابراین باید تمامی ابزارهای لازم را در اختیار داشته باشید تا سختی و زحمت در طول تستگرفتن به حداقل ممکن برسد. pytest
یکی از بهترین ابزارهایی است که میتوانید به جهت افزایش سرعت در فرایند تست برنامههای Python، از آن بهره بگیرید.
مطالبی که در این آموزش یادمیگیرید:
pytest
چه امکاناتی را در اختیار شما میگذارد.- اطمینان از اینکه تستهایتان مستقل از وضعیت هستند.
- چگونه تستهای تکراری را بیشتر قابل فهم کنید.
- چگونه زیرمجموعهای از تستها را با استفاده از نام و یا گروههای مشخص شده، اجرا کنید.
- چگونه تستهایی ایجاد و مدیریت کنید که در شرایط دیگر هم قابل استفاده باشد.
نحوه نصب pytest
برای دنبالکردن مثالهای این آموزش ابتدا نیاز دارید که pytest
را نصب کنید. مانند سایر پکیجهای پایتون، میتوانید pytest
را در محیطهای مجازی (virtual environments)، از PyPI توسط دستور pip
، نصب کنید:
$ python -m pip install pytest
اگر دستور نصب pytest
را در یک محیط مجازی اجرا کنید، میتوانید از دستور pytest
در آن محیط استفاده کنید، اما اگر از محیط مجازی استفاده نمیکنید، با اجرای این دستور، pytest
در کل سیستمتان نصب خواهد بود.
چه چیزی باعث میشود که pytest
بسیار کاربردی و مفید باشد؟
اگر قبلا برای کدهای پایتونیتان تست نوشته باشید، حتما از ماژول داخلی پایتون به نام unittest
استفاده کردهاید. unittest
پایه و اساس خوبی برای ایجاد تستهای مناسب را فراهم میکند، اما دارای کاستیهای اندکی هم میشود.
تعدادی فریمورک تلاش کردهاند که مشکلها و مسائل موجود در استفاده از unittest
را مطرح و حل کنند، pytest
یکی از معروفترینها در اثبات این قضیه است. pytest
یک پکیج با قابلیتهای زیاد است که در آن میتوان از افزونههای مختلف برای تستگرفتن کدهای پایتونی استفاده کرد.
اگر تاکنون از pytest
استفادهای نکردهاید، پس شما باید حتما بدنبال آن بروید. زیرا استفاده از آن باعث میشود تا فرایند تست کاربردیتر و آسانتری داشته باشید. با استفاده از pytest
، انجام وظایف متداول به کد کمتری نیاز دارد، همچنین باعث صرفهجویی در زمان، با استفاده از دستورها و افزونهها میشود. این پکیج تستهای قبلی شما، حتی آنهایی که با unittest
نوشته شدهاند را نیز اجرا میکند.
مانند سایر فریمورکها، برخی از الگوهای توسعه که برای اولین بار از آنها استفاده میکنید، با افزایش اندازه کد و تستهای شما باعث ایجاد سردرگمی و اعصابخوردی میشود. این آموزش به شما کمک میکند تا بتوانید از برخی ابزارهایی که pytest
فراهم میکند استفاده کنید و آنها را بفهمید، تا در هر مقیاسی فرآیند تستگیریتان کارآمد و مفید باشد.
کاهش تکرار مکررات
بیشتر تستهایی که بر اساس توابع هستند از مدل ترتیب-عمل-اثبات (Arrange-Act-Assert) پیروی میکنند:
- ترتیب (Arrange) یا تنظیم شرطها برای تست.
- عمل (Act) توسط اجرای توابع و یا متدها.
- اثبات (Assert) اینکه یک شرط صحیح است.
فریمورکهای این چنینی معمولا از assert
هایتان استفاده میکنند که باعث میشود هنگامی که یک assert
با شکست مواجه میشود، اطلاعات مفیدی بدست بیاید. برای مثال unittest
تعداد زیادی از ابزارهای مفید برای بررسی assert
ها را فراهم میکند. به هرحال حتی مجموعه کوچکی از تستها نیز به کدی با تکرار کمتر نیاز دارد.
تصور کنید قصد دارید تستی بنویسید تا از کارکرد unittest
در پروژهتان مطمئن شوید. شاید بخواهید تستی بنویسید که برای همیشه موفق (pass
) شود و یا حتی تستی داشته باشید که همیشه با شکست (fail
) مواجه شود:
# test_with_unittest.py
from unittest import TestCase
class TryTesting(TestCase):
def test_always_passes(self):
self.assertTrue(True)
def test_always_fails(self):
self.assertTrue(False)
حالا میتوانید این تستها را از طریق خط فرمان، با استفاده از گزینه discover
در unittest
اجرا کنید:
$ python -m unittest discover
F.
============================================================
FAIL: test_always_fails (test_with_unittest.TryTesting)
------------------------------------------------------------
Traceback (most recent call last):
File "/.../test_with_unittest.py", line 9, in test_always_fails
self.assertTrue(False)
AssertionError: False is not True
------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
از آنجایی که انتظار داشتیم، یکی از تستها موفق و دیگری با شکست روبهرو شد. حالا شما اثبات کردید که unittest
به خوبی کار میکند، اما توجه داشته باشید که برای این کار چه مراحلی را باید انجام دهید:
- ابتدا باید کلاس
TestCase
را ازunittest
وارد کدتان کنید. - کلاس
TryTesting
را که فرزندی (subclass
) از کلاسTestCase
است را ایجاد کنید. - برای هر مورد تست باید یک تابع در کلاس
TryTesting
ایجاد کنید. - توسط یکی از توابع
self.assert*
از کلاسunittest.TestCase
، برای ایجادassert
استفاده کنید.
اینها مقدار مشخصی از کدهایی است که باید بنویسید، به این دلیل که اینها برای کوچکترین حالت ممکن هستند، باید آنها را برای هر تستی بنویسید، به عبارتی مجبور هستید که یک تکه کد را بارها و بارها بنویسید. pytest
این کار را با استفاده از کلمه کلیدی assert
که در پایتون استفاده میشود، ساده کرده است:
# test_with_pytest.py
def test_always_passes():
assert True
def test_always_fails():
assert False
این تمام کاری است که باید انجام دهید. مجبور نیستید که با هیچ import
و یا کلاسی سر و کله بزنید. به دلیل استفاده از کلمه کلیدی assert
، نیازی به یادگیری تمام تفاوتهای میان توابع self.assert*
در unittest
ندارید. اگر نیاز دارید عبارتی را بنویسید که انتظار دارید مقدار آن برابر با True
باشد، pytest
این کار را برای شما انجام خواهد داد. میتوانید تست را با دستور pytest
اجرا کنید:
$ pytest
================== test session starts =============================
platform darwin -- Python 3.7.3, pytest-5.3.0, py-1.8.0, pluggy-0.13.0
rootdir: /.../effective-python-testing-with-pytest
collected 2 items
test_with_pytest.py .F [100%]
======================== FAILURES ==================================
___________________ test_always_fails ______________________________
def test_always_fails():
> assert False
E assert False
test_with_pytest.py:5: AssertionError
============== 1 failed, 1 passed in 0.07s =========================
pytest
نتیجه متفاوتتری نسبت به unittest
ارائه میکند. مواردی که خروجی این دستور ارائه میکند:
- وضعیت سیستم، ازجمله نسخه پایتون،
pytest
و هر افزونهای که نصب کردهاید. rootdir
و یا پوشهای که در آن به دنبال تنظیمات و تستها میگردد.- تعداد تستهایی که اجرا کننده به دست آوردهاست.
خروجی وضعیت هر تست را با شیوهای مشابه unittest
نشان میدهد:
- نقطه (
.
) به این معنی است که تست موفق بوده است. F
به این معنی است که تست شکست خورده است.E
به این معنی است که تست با یک خطای غیرمنتظره (unexpected exception
) روبهرو شده است.
برای تستهایی که با شکست روبهرو شدهاند، خروجی جزئیات کاملی از نتیجه شکست تست را ارائه میکند. در مثال بالا، تست به این دلیل شکست خورد که assert False
همیشه با شکست روبهرو میشود. در نهایت خروجی، گزارشی از وضعیت مجموعه تستها است.
مثالهای کوچک و بیشتری از assert
ها:
def test_uppercase():
assert "loud noises".upper() == "LOUD NOISES"
def test_reversed():
assert list(reversed([1, 2, 3, 4])) == [4, 3, 2, 1]
def test_some_primes():
assert 37 in {
num
for num in range(1, 50)
if num != 1 and not any([num % div == 0 for div in range(2, num)])
}
یادگیری pytest
بسیار راحتتر از unittest
است، زیرا نیازی به یادگیری ساختارهای جدید برای تستها نیست. همچنین استفاده از assert
، که حتی ممکن است در کدهای قبلیتان از آن استفاده کرده باشید، باعث میشود کد شما خواناتر و قابلفهمتر باشد.
وضعیت و مدیریت وابستگیها
گاهی اوقات تستهایتان به دیتای دیگر و یا یک سری تست مضاعف، برای سایر آبجکتها در کدتان نیاز دارند. در unittest
، شاید این وابستگیها را توسط متدهای setUp()
و tearDown()
خارج و قابل استفاده کنید (extract
) تا هر تست در هر کلاسی بتواند از آنها استفاده کند. اما در انجام این کار، سهوا تست را به یک بخشی از دیتا و یا یک آبجکت به صورت کاملا ضمنی، وابسته کنید.
در طول زمان، وابستگیهای ضمنی و کناری میتواند باعث پیچیدگی و درهم ریختگی کد شود که این قضیه شما را مجبور میکند تا این مشکلها را برطرف کنید که بتوانید تستهای خود را بفهمید. تستها به شما کمک میکند تا کدتان را بیشتر قابل فهم کنید. اگر درک خود تستها نیز برایتان دشوار باشد، احتمالا در دردسر افتادهاید.
pytest
راههای متفاوتی را دربردارد. این منجر میشود تا شما به سمت تعریف صریح وابستگیها هدایت شوید، بنابهدلیل وجود قابلیت fixture
های pytest
، اینها قابل انجام هستند. fixture
های pytest
توابعی هستند که دیتا و یا تستهای مضاعف تولید میکنند و یا وضعیت سیستم را برای مجموعهای از تستها، ایجاد میکند. هر تستی که بخواهد از fixture
استفاده کند، باید آن را صریحا به عنوان ورودی قبول کند، بنابراین وابستگیها همیشه در ابتدا باید معرفی شوند.
همچنین fixture
ها میتوانند سایر fixture
ها را، با تعریف صریح آنها به عنوان وابستگی، ایجاد کنند. این به این معناست که در طول زمان، fixture
هایتان میتوانند حجیم و ماژولار شوند. بنابهاینکه میتوان fixture
ها را داخل سایر fixture
ها وارد کرد، انعطافپذیری زیادی در اختیار ما قرار میگیرد. همچنین مدیریت وابستگیها، هرچقدر مجموعه تستها بزرگتر شوند، چالشبرانگیزتر میشوند. در ادامه این آموزش اطلاعات بیشتری در رابطه با fixture
ها و تکنیکهایی برای حل این چالشها میآموزید.
فیلترکردن تستها
هرچقدر مجموعه تستهای شما بزرگتر و بیشتر میشود، شاید بخواهید تعداد کمی از تستها را بر روی یک قابلیت مشخص اجرا کنید و اجرای تمام مجموعه تستها را به زمان دیگری موکول کنید، چرا که در حال حاضر نیازی به آن نیست. pytest
چند راه برای انجام این کار فراهم میکند:
- فیلتر براساس نام: میتوانید
pytest
را به اجرای تستهایی محدود که نام آنها با یک عبارت خاص تطابق دارد. این کار را با سوییچ-k
میتوانید انجام دهید. - محدودکردن پوشه: به طور پیشفرض،
pytest
در پوشهای که تستها در آن قرار دارند و یا پوشه فعلی اجرا میشود. - دستهبندی تست:
pytest
میتواند تستها را بر دستهبندیهای خاصی که خودتان تعریف میکنید، اجرا کند و یا حتی برعکس، همه تستها غیر از دستهبندی که شما تعیین میکنید را اجرا کند. این کار را با سوییچ-m
میتوانید انجام دهید.
دستهبندی تستها به طور خاص یک ابزار ظریف و قدرتمند است. pytest
این امکان را به شما میدهد که علامتها (mark
) و یا لیبلهای مخصوص برای هر تستی که بخواهید را ایجاد کنید. یک تست ممکن است چندین لیبل داشته باشد. در ادامه این آموزش، نحوه کار و استفاده از علامتها و لیبلها را در pytest
، خواهید آموخت.
پارامترسازی تستها
وقتی در حال تست تابعی هستید که دیتا پردازش میکند و یا تغییرهایی کلی بر روی ورودی اجرا میکند، متوجه خواهید شد که در حال نوشتن تستهای مشابه به هم هستید، که ممکن است تنها در ورودی و خروجی کدی که مورد تست قرار گرفته است، تفاوت داشته باشند. این کار باعث تکرار کد تست میشود و انجام این کار باعث مبهمشدن رفتار موردی میشود که شما میخواهید از آن تست بگیرید.
unittest
راهی برای جمعآوری چندین تست و تبدیل آنها به یک تست فراهم میکند، اما در پایان تست، نتیجه هر کدام از تستها را جداگانه نمایش نمیدهد. اگر یکی از تستها با شکست روبرو شود اما مابقی با موفقیت به پایان برسند، در نهایت تمامی تستها یک نتیجه دارند و آن هم عدم موفقیت خواهد بود. pytest
راهحل خود را برای این موضوع ارائه میدهد که هر کدام از تستها به طور مستقل میتوانند موفق شوند و یا شکست بخورند. با این موضوع در ادامه بیشتر آشنا خواهید شد.
معماری براساس افزونهها
یکی از بهترین قابلیتهای pytest
، آزادی در شخصیسازی و قابلیتهای جدید آن است. هر بخش از کد میتواند جدا شود و دستخوش تغییر شود. با این تفاسیر، کاربران pytest
مجموعه بزرگی از افزونههای مفید را توسعه دادهاند.
بااینحال برخی از افزونههای pytest
بر روی فریمورکهای مخصوصی، همچون جنگو، تمرکز میکنند، مابقی با سایر مجموعههای تستها مناسب هستند. در ادامه این آموزش اطلاعات بیشتری در رابطه با برخی از افزونههای مخصوص، خواهید آموخت.
fixture
ها: مدیریت وضعیت و وابستگیها
fixture
های pytest
راهی برای فراهمسازی داده، تستهای مضاعف و یا ایجاد وضعیت برای تستهای شماست. fixture
ها توابعی هستند که میتوانند گستره بزرگی از مقادیر را برگردانند. هر تستی که به یک fixture
وابسته است، باید صریحا آن fixture
را به عنوان ورودی داشته باشد.
چه زمانی از fixture
ها استفاده کنیم؟
تصور کنید که در حال نوشتن تابعی با نام format_data_for_display()
هستید، که وظیفه آن پردازش دادهای است که توسط API
به آن رسیده است. دیتای دریافتی، لیستی از مردم، به همراه نام، نامخانوادگی و نام شغل آنهاست. این تابع باید نام کامل آنها (نام و نامخانوادگی و یا given_name
به همراه family_name
) افراد به همراه :
و عنوان شغل آنها را در خروجی نمایش بدهد.
def format_data_for_display(people):
... # Implement this!
def test_format_data_for_display():
people = [
{
"given_name": "Alfonsa",
"family_name": "Ruiz",
"title": "Senior Software Engineer",
},
{
"given_name": "Sayid",
"family_name": "Khan",
"title": "Project Manager",
},
]
assert format_data_for_display(people) == [
"Alfonsa Ruiz: Senior Software Engineer",
"Sayid Khan: Project Manager",
]
حالا تصور کنید به تابع دیگری برای تبدیل این داده به حالتی که مقادیر با کاما از یکدیگر جدا شدهاند، برای استفاده در اکسل نیاز دارید. کد تست در بدترین حالت مانند مثال زیر است:
def format_data_for_excel(people):
... # Implement this!
def test_format_data_for_excel():
people = [
{
"given_name": "Alfonsa",
"family_name": "Ruiz",
"title": "Senior Software Engineer",
},
{
"given_name": "Sayid",
"family_name": "Khan",
"title": "Project Manager",
},
]
assert format_data_for_excel(people) == """given,family,title
Alfonsa,Ruiz,Senior Software Engineer
Sayid,Khan,Project Manager
"""
اگر متوجه شدید که در چندین تست از یک سری داده پایهای یکسان استفاده میکنید، احتمالا fixture
ها در آینده به کار شما خواهند آمد. میتوانید داده تکراری را در یک تابع که به همراه دکوریتور @pytest.fixture
تعریف شده است، قرار دهید، این کار نشان میدهد که این تابع یک fixture
است:
import pytest
@pytest.fixture
def example_people_data():
return [
{
"given_name": "Alfonsa",
"family_name": "Ruiz",
"title": "Senior Software Engineer",
},
{
"given_name": "Sayid",
"family_name": "Khan",
"title": "Project Manager",
},
]
میتوانید از fixture
یی که ساختهاید، به عنوان ورودی در تستهای خودتان استفاده کنید. مقدار آن، مقدار بازگشتی از تابع fixture
است:
def test_format_data_for_display(example_people_data):
assert format_data_for_display(example_people_data) == [
"Alfonsa Ruiz: Senior Software Engineer",
"Sayid Khan: Project Manager",
]
def test_format_data_for_excel(example_people_data):
assert format_data_for_excel(example_people_data) == """given,family,title
Alfonsa,Ruiz,Senior Software Engineer
Sayid,Khan,Project Manager
"""
هرکدام از تستها به مراتب کوتاهتر شدهاند و یک رابطه واضح با دیتایی که به آن وابسته هستند، دارند. مطمئن شوید که نامی که به fixture
تان میدهید یک نام مخصوص باشد. از این طریق در نوشتن تستها در آینده، به راحتی میدانید از کدام از fixture
ها استفاده کنید.
چه زمانی از fixture
ها استفاده نکنیم؟
fixture
ها ابزار خوبی برای خارجکردن (extract
) دیتا و یا آبجکتهایی است که قصد دارید از آنها در تستهای مختلف استفاده کنید. اما در تستهایی که تغییرهای جزئی در دیتای آنها وجود دارد مناسب نیستند. استفاده از fixture
ها در تستها، از استفاده از دیتای خام یا آبجکتها بهتر نیست. حتی میتواند به دلیل افزودن لایههای بیشتر و یا غیرضروری به دادهها، بدتر هم باشد.
مانند سایر مباحث، این موضوع هم نیاز به تمرین و فکر بر روی آنها دارد تا بتوان سطح و زمان مناسب استفاده از آنها را متوجه شد.
fixture
ها در مقیاسهای مختلف
هنگامی که دادهها را از تستهای خود، توسط fixture
ها خارج میکنید، متوجه این شوید که برخی از fixture
ها میتوانند از خارجکردن (extract
) بهره بیشتری ببرند. fixture
ها ماژولار هستند، بنابراین میتوانند به سایر fixture
ها نیز وابسته باشند. حتی شاید متوجه این موضوع شوید که fixture
هایی در ۲ ماژول تست کاملا جدا، یک وابستگی مشترک را به اشتراک میگذارند. در این مورد چه کاری خواهید کرد؟
میتوانید fixture
ها را از ماژولهای تست به ماژولهای عمومی fixture
ها منتقل کنید. از این طریق، میتوانید آنها را در هر ماژول تستی که بخاهید، وارد کنید. این راهحل خوبی است برای زمانی که متوجه میشوید از برخی fixture
ها به کرات در پروژه خود استفاده میکنید.
pytest
در سرتاسر ساختار پوشه به دنبال ماژول conftest.py
میگردد. هر ماژول conftest.py
تنظیمات را برای فایلهایی که pytest
پیدا میکند، فراهم میکند. میتوانید از هر fixture
یی که در یک conftest.py
مشخص، در دایرکتوری اصلی و هر سابدایرکتوی، تعریف شده است، استفاده کنید، که بهترین مکان برای قراردادن fixture
هایی است که به صورت گسترده از آنها استفاده شده است.
یکی دیگر از موارد استفاده از fixture
ها، محافظت از منابع در مقابل دسترسی تستها به آنها است. تصور کنید که مجموعهای از تستهایی دارید که قرار است با API
سروکله بزند. قصد دارید مطمئن شوید که تستها هیج درخواست واقعی در شبکه ایجاد نمیکنند، حتی اگر یک تست به صورت تصادفی کد یک درخواست واقعی در شبکه را اجرا کند. pytest
برای این موضوع fixture
یی با عنوان monkeypath
فراهم میکند که میتواند مقادیر و رفتارها را تغییر دهد، که میتوانید از این fixture
برای موارد زیادی استفاده کنید:
# conftest.py
import pytest
import requests
@pytest.fixture(autouse=True)
def disable_network_calls(monkeypatch):
def stunted_get():
raise RuntimeError("Network access not allowed during testing!")
monkeypatch.setattr(requests, "get", lambda *args, **kwargs: stunted_get())
با جایگزینی disable_network_calls()
در conftest.py
و افزودن گزینه autouse=True
، میتوانید مطمئن شوید که ایجاد درخواست در شبکه، برای هرکدام از تستها در این مجموعه تست، غیرفعال است. هرکدام از تستهایی که کد requests.get()
را اجرا کند، باعث ایجاد خطای RuntimeError
میشود، که به این معنی است یک درخواست غیرمنتظره در شبکه ایجاد شده است.
علامتها: دستهبندی تستها
در هر مجموعه بزرگی از تستها، برخی از تستها ناخواسته کند خواهند بود. ممکن است اجرای آنها با timeout
روبرو شود، برای مثال ممکن است بخش عظیمی از کد را مورد آزمایش قرار بدهند. دلیل هرچه باشد، عدم اجرا یا پیشگیری از اجرای تمام تستها به هنگامی که میخواهید یک قابلیت را به تنهایی تست کنید، نکته بسیار مهم و مثبتی است.
pytest
این اجازه را به شما میدهد که دستهبندیهای مختلف برای تستهایتان ایجاد کنید، همچنین این امکان را برای شما فراهم میکند تا به هنگام اجرا مجموعه تستتان، از برخی از دستهها استفاده و یا از برخی دیگر چشم پوشی کنید. شما یک تست را با هر تعداد از دستهبندیهایی که بخواهید، میتوانید علامت گذاری کنید.
علامتگذاری تستها براساس سابسیستم و یا وابستگیها، کار مفید و کارآمدی است. برای مثال اگر برخی از تستهای شما به دسترسی و اتصال به دیتابیس نیاز داشته باشند، میتوانید توسط @pytest.mark.database_access
آنها را علامتگذاری کنید.
با توجه به اینکه برای این علامتها هر اسمی که بخواهید میتوانید تنظیم کنید، اشتباه تایپی و یا فراموشی اسم یک علامت امکانپذیر خواهد بود. pytest
وجود علامتهای غیرقابل تشخیص را به شما گوشزد خواهد کرد.
با استفاده از سوییچ --strict-markers
در دستور pytest
این اطمینان به شما داده میشود که تمام علامتهای موجود در تست، در تنظیمات pytest
رجیستر شده است. این قضیه مانع اجرای تستهای شما میشود، تا زمانیکه علامتهای ناشناخته و رجیستر نشده را رجیستر کنید. برای اطلاعات بیشتر در رابطه با علامتها در pytest
به این لینک مراجعه کنید.
هنگامی که زمان اجرای تستهای شما فرا میرسد، میتوانید تمامی آنها را به صورت پیشفرض، توسط دستور pytest
اجرا کنید. اما اگر شما نیاز به اجرای آن دسته از تستهایی که باید به دیتابیس متصل شوند، میتوانید از دستور زیر استفاده کنید:
pytest -m database-access
برای اجرای تمام تستها به غیر از آنهایی که باید به دیتابیس متصل شوند هم، میتوانید از دستور زیر استفاده کنید:
pytest -m "not database_access"
همچنین میتوانید از fixture
یی تحت عنوان autouse
استفاده کنید تا دسترسی و اتصال به دیتابیس، برای تستهایی که توسط database_access
علامتگذاری شدهاند را محدود کنید.
برخی از افزونهها کارکرد علامتها را با محافظت از منابع در مقابل دسترسی تستها به آنها، افزایش میدهند. افزونه pytest-django
علامت django_db
را فراهم میکند. هر کدام از تستها که فاقد این علامت باشند و تلاش کنند که به دیتابیس دسترسی پیدا کنند، با شکست مواجه خواهند شد. اولین تستی که تلاش کند به دیتابیس متصل شود، باعث ایجاد دیتابیس تستی جنگو خواهد شد.
برای اینکه بتوانید از علامت django_db
استفاده کنید، باید تمام وابستگیها را به صورت صریح و واضح بیان کنید. این فلسفه و منطق pytest
است. این قضیه از طرف دیگر به این معناست که میتوانید تستهایی که نیاز ندارند به دیتابیس متصل شوند را با سرعت بیشتری اجرا کنید، به دلیل اینکه دستورpytest -m "not django_db"
از ایجاد دیتابیس توسط تست، جلوگیری میکند. صرفهجویی در زمان در اینجا مهم میشود، به خصوص اگر شما بخواهید تستهای خود را بارها اجرا کنید.
pytest
به خودی خود، یک سری علامت برای شما فراهم میکند:
skip
بی قید و شرط یک تست را نادیده میگیرد.skipif
اگر شرط صحیح باشد، تست را نادیده میگیرد.xfail
به این معناست که تست مدنظر باید شکست بخورد، بنابراین حتی اگر تست موفق نشود، کل مجموعه تست در وضعیت موفقیتآمیز اجرا خواهد شد.parametrize
تستهای مختلف با مقادیر مختلف به عنوان ورودی ایجاد میکند. با این علامت در ادامه آشنا خواهید شد.
میتوانید لیست تمامی علامتهایی که pytest
، بدون استفاده از افزونهای، میشناسد را با دستور زیر مشاهده کنید:
pytest --markers
پارامترسازی: ادغام تستها
قبلتر در این آموزش، نحوه کاهش تکرار با خارجکردن وابستگیها، با استفاده از fixture
های pytest
را مشاهده کردید. fixture
ها زمانی که چندین تست با ورودیهای متفاوت و خروجیهای مدنظرتان داشته باشید، مفید نخواهد بود. در این موارد میتوانید تعریف یک تست را پارامترسازی کنید، pytest
تستهای گوناگون با پارامترهایی که شما مشخص میکنید، میسازد.
تصور کنید که تابعی دارید که چک میکند یک string
متقارن و یا وارون است یا خیر. تستهای اولیه مانند زیر است:
def test_is_palindrome_empty_string():
assert is_palindrome("")
def test_is_palindrome_single_character():
assert is_palindrome("a")
def test_is_palindrome_mixed_casing():
assert is_palindrome("Bob")
def test_is_palindrome_with_spaces():
assert is_palindrome("Never odd or even")
def test_is_palindrome_with_punctuation():
assert is_palindrome("Do geese see God?")
def test_is_palindrome_not_palindrome():
assert not is_palindrome("abc")
def test_is_palindrome_not_quite():
assert not is_palindrome("abab")
تمامی این تستها، به غیر از دو مورد آخر، ساختار مشابه و یکسانی دارند:
def test_is_palindrome_<in some situation>():
assert is_palindrome("<some string>")
میتوانید از @pytest.mark.parametrize()
برای تکمیل این ساختار با مقادیر مختلف استفاده کنید، خیلی چشمگیر و قابل ملاحظه کد تست شما کمتر میشود:
@pytest.mark.parametrize("palindrome", [
"",
"a",
"Bob",
"Never odd or even",
"Do geese see God?",
])
def test_is_palindrome(palindrome):
assert is_palindrome(palindrome)
@pytest.mark.parametrize("non_palindrome", [
"abc",
"abab",
])
def test_is_palindrome_not_palindrome(non_palindrome):
assert not is_palindrome(non_palindrome)
ورودی اول تابع parametrize()
، نام پارامترها میان دابل کوتیشن است. ورودی دوم میتواند list
و یا tuple
و یا حتی یک مقدار باشد که مقادیر یا مقدار پارامترها را نشان میدهد. میتوانید پارامترسازی را یک قدم پیشتر ببرید و همه تستهایتان را در یک تست ادغام کنید:
@pytest.mark.parametrize("maybe_palindrome, expected_result", [
("", True),
("a", True),
("Bob", True),
("Never odd or even", True),
("Do geese see God?", True),
("abc", False),
("abab", False),
])
def test_is_palindrome(maybe_palindrome, expected_result):
assert is_palindrome(maybe_palindrome) == expected_result
حتی با اینکه کد شما را کوتاهتر کرد، به مورد توجه داشته باشید که باعث شفافتر شدن کد شما نمیشود. از پارامترسازی برای جدا سازی دیتای مخصوص تست از تست و نحوه رفتار آن استفاده کنید، به نحوی که معلوم میشود این تست، چه تستی است.
گزارش زمانی: مبارزه با تستهای کند
هرزمان که کدها را از حالت پیادهسازی به حالت تستگرفتن میبرید، شما متحمل سربار اضافه خواهید شد. اگر تستهایتان در ابتدا کند باشند، باعث خستگی و کاهش سرعت پیشرفت کار میشود.
در قسمتهای قبلی در رابطه با علامتگذاری آموختید که چگونه میتوانید تستهای کند را از مجموعه خود خارج کنید. اگر قصد داشتهباشید تا سرعت اجرای تستهای خود را افزایش دهید، دانستن اینکه کدام تستها کند هستند باعث پیشرفت زیادی در پیادهسازی و فرآیند تست خواهد شد. pytest
به صورت خودکار مدت زمان اجرای تستها را برای شما اندازهگیری میکند و شما را از آنهایی که نسبت به بقیه زمان بیشتری گرفتهاند، مطلع میسازد.
از دستور pytest
به همراه سوییچ --durations
استفاده کنید تا بتوانید مدت زمان اجرای تستها را در خروجی ببینید. سوییچ --durations
نیاز به یک مقدار صحیح، مثلا n
، دارد تا n
تستی که نسبت به بقیه کندتر هستند را در خروجی نمایش دهد. خروجی نتیجه تستهای شماست:
$ pytest --durations=3
3.03s call test_code.py::test_request_read_timeout
1.07s call test_code.py::test_request_connection_timeout
0.57s call test_code.py::test_database_read
======================== 7 passed in 10.06s ==============================
هر کدام از تستهایی که در خروجی مشاهده میکنید، کاندیدای خوبی برای حذف و در نتیجه افزایش سرعت تست هستند، زیرا مقدار زمان بیشتری نسبت به میانگین زمان کل تست گرفتهاند.
حواستان باشد که برخی از تستها ممکن است باعث ایجاد سربار به صورت مخفی و دور از چشم ما شوند. قبلتر در رابطه با چگونگی علامتگذاری اولین تست توسط django_db
که باعث ایجاد دیتابیس تست جنگو میشود، خواندید. گزارش و خروجی مدت زمان لازم برای راهاندازی این دیتابیسی که در تست ایجاد شدهاست را نشان میدهد، که میتواند گمراهکننده باشد.
افزونههای کارآمد pytest
در رابطه با برخی از افزونههای کارآمد و ارزشمند pytest
قبلتر چیزهایی آموختید. میتوانید آنها و برخی دیگر را عمیقتر در ادامه بررسی کنید.
pytest-randomly
pytest-randomly
به ظاهر، کار ساده اما ارزشمندی را انجام میدهد: این افزونه شما را وادار میکند تا تستهایتان را در یک توالی رندوم و اتفاقی اجرا کنید. pytest
همیشه تمام تستهایی را که بتواند قبل از اجرا پیدا کند را، جمعآوری میکند، بنابراین pytest-randomly
توالی این لیست را قبل از اجرا به هم میریزد.
استفاده از آن راه خوبی برای آشکار شدن تستهایی است که نیازمند اجرا در یک توالی خاص هستند، که به این معناست که آنها به صورت کامل به برخی دیگر از تستها وابسته هستند. اگر مجموعه تستتان را از پایه با استفاده از pytest
بنویسید، چنین چیزی اتفاق نخواهد افتاد و بیشتر شبیه مجموعه تستهایی خواهد شد که به pytest
منتقل کردهاید.
این افزونه مقداری را در توضیح تنظیمات نمایش میدهد. میتوانید از آن مقدار برای اجرا تستها در یک توالی یکسان استفاده کنید تا بتوانید مشکلتان را حل کنید.
pytest-cov
اگر اینکه تستهایتان چقدر کدتان را پوشش میدهد را اندازهگیری کرده باشید، احتمالا از پکیج coverage
استفاده کردهاید. pytest-cov
با coverage
کارمیکند، بنابراین میتوانید جهت مشاهده گزارش از مقدار این پوشش از این دستور استفاده کنید: pytest --cov
pytest-django
pytest-django
برای کار با تستهای جنگو، fixture
و علامتهای کارآمد و مفیدی را فراهم میکند. قبلتر در این آموزش، نشانه و یا علامت django_db
را دیدهاید و یا fixture
تحت عنوان rf
، که دسترسی مستقیم به نمونهای از RequestFactory
جنگو را فراهم میکند. همچنین fixture
دیگری با عنوان settings
که راه سریع برای تغییر و بازنویسی تنظیمات جنگو را فراهم میکند. این افزونه، ابزاری بسیار عالی برای سرعتبخشیدن به تست پروژه جنگوتان است.
pytest-bdd
pytest
میتواند برای اجرای تستهای خارج از محدوده تست معمولی استفاده شود. توسعه براساس رفتار (behavior-driven development)، توسعهدهندگان را به نوشتن توضیحات به زبان و شکل ساده، همانند اعمال و توقعات کاربران، تشویق میکند، که میتوان از آنها برای تشخیص زمان برای پیادهسازی یک قابلیت استفاده کرد. pytest-bdd
از طریق استفاده از Gherkin برای نوشتن تست برای قابلیتها استفاده کرد.
برای مشاهد لیست افزونههای موجود برای pytest
، میتوانید به این لینک مراجعه کنید.
جمعبندی
pytest
قابلیتهای زیادی را برای فیلتر و بهبود تستهایتان، توسط افزونهها که ارزش خودشان را بیشتر میکنند، در اختیارتان میگذارد. گرچه شما مجموعه بزرگی از تستهایی که با unittest
برای یک پروژه جدیدتان که از صفر نوشته شده است را داشته باشید، pytest
بازهم موارد و امکانات زیادی برای ارائه به شما دارد.
در این آموزش شما آموختید که از موارد زیر استفاده کنید:
- Fixtureها برای مدیریت وابستگیها، وضعیت و قابلیت استفاده مجدد.
- علامتها (Mark) برای دستهبندی تستها و محدودسازی آنها در دسترسی به منابع.
- پارامترسازی (Parametrization) برای کاهش کدهای تکراری میان تستها.
- مدیریت زمان (Durations) برای تشخیص کندترین تست.
- افزونهها (Plugin) برای هماهنگی با سایر فریمورکها و یا دیگر ابزارهای مخصوص تست.
pytest
را نصب کنید و آن را امتحان کنید. مطمئنا پشیمان نخواهید شد.
منبع: https://realpython.com/pytest-python-testing
همچنین بخوانید: معرفی هاست رایگان جنگو