برنامه‌نویسی

تست کدهای پایتون توسط pytest


۳۱ تیر ۱۳۹۹
تست کدهای پایتون توسط pytest

تست‌گرفتن از کدهایتان در یک پروژه، مزایای بسیاری دارد. این قضیه باعث افزایش اعتماد به نفس شما می‌شود، زیرا که کدتان همانطوری که انتظارش را داشته‌اید، رفتار کرده است و این اطمینان را می‌دهد که تغییرات کدتان باعث بازگشت به عقب نمی‌شود. نوشتن و مدیریت تست‌ها برای کدتان کار سختی است، بنابراین باید تمامی ابزارهای لازم را در اختیار داشته باشید تا سختی و زحمت در طول تست‌گرفتن به حداقل ممکن برسد. pytest یکی از بهترین ابزارهایی است که می‌توانید به جهت افزایش سرعت در فرایند تست، از آن بهره بگیرید.

مطالبی که در این آموزش یادمی‌گیرید:

  • 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) پیروی می‌کنند:

  1. ترتیب (Arrange) یا تنظیم شرط‌ها برای تست.
  2. عمل (Act) توسط اجرای توابع و یا متدها.
  3. اثبات (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 به خوبی کار می‌کند، اما توجه داشته باشید که برای این کار چه مراحلی را باید انجام دهید:

  1. ابتدا باید کلاس TestCase را از unittest وارد کدتان کنید.
  2. کلاس TryTesting را که فرزندی (subclass) از کلاس TestCase است را ایجاد کنید.
  3. برای هر مورد تست باید یک تابع در کلاس TryTesting ایجاد کنید.
  4. توسط یکی از توابع 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 ارائه می‌کند. مواردی که خروجی این دستور ارائه می‌کند:

  1. وضعیت سیستم، ازجمله نسخه پایتون، pytest و هر افزونه‌ای که نصب کرده‌اید.
  2. rootdir و یا پوشه‌ای که در آن به دنبال تنظیمات و تست‌ها می‌گردد.
  3. تعداد تست‌هایی که اجرا کننده به دست آورده‌است.

خروجی وضعیت هر تست را با شیوه‌ای مشابه 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