نحوه نوشتن Doctest در پایتون

نحوه نوشتن Doctest در پایتون
Avatar
نویسنده: علیرضا برزودی
پنج‌شنبه 25 فروردین 1401
مطالعه: ۱۵ دقیقه ۰ نظر ۱۲۶۵ بازدید

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

در واقع نوشتنِ داکیومنت و تست آن قبل از کدنویسی به برنامه‌نویس بسیار کمک می‌کند. زیرا برنامه‌نویس با انجام این کار اطمینان حاصل می‌کند که تابع کدنویسی شده(بطور مثال)، به خوبی بر روی آن فکر شده و تنها به بررسی موارد احتمالی می‌پردازد.

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

زبان برنامه نویسی پایتون دارای قابلیت‌های زیادی است. از جمله این قابلیت‌ها می‌توان به ماژولِ فریمورکی به نام Doctest اشاره کرد. این ماژول به صورت برنامه‌نویسی، کد پایتون را برای بخش‌هایی از text در کامنت‌ها -که شبیه به sessionهای پایتون interactive هستند- جستجو می‌کند. سپس، ماژول آن sessionها را اجرا می‌کند تا تأیید کند که کد رفرنس داده شده توسط Doctest، مطابق انتظارات اجرا می‌شود.

اما چگونه می‌توانیم در پایتون Doctest بنویسیم؟ در ادامه این مقاله از پارس پک به آموزش قدم به قدم نحوه نوشتن Doctest می‌پردازیم. پس با ما همراه باشید.

نکته: برای استفاده از قابلیت Doctest شما باید نخست پایتون 3 را نصب کرده و یک محیط برنامه نویسی را ایجاد کنید.

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

نوشتن Doctest در پایتون شبیه به نوشتن یک کامنت است، با این تفاوت که سه علامت کوتیشن (“””) در ابتدا و انتهای آن قرار می‌گیرد. گاهی Doctestها با مثالی از تابع و خروجی مورد انتظار نوشته می‌شوند. اما برخی اوقات شاید توضیحاتی نیز در مورد آنچه که تابع قصد انجام آن را دارد نیز آورده شود.

درج comment نشان می‌دهد که شما به عنوان برنامه‌نویس اهداف خود را واضح‌تر بیان کرده‌اید و فردی که کد را می‌خواند آن را به خوبی درک می‌کند. به بیانی عامه‌تر، یکی از شرایط کدزنی تمیز و مرتب، استفاده به موقع از کامنت ها برای ارائه توضیحات لازم است!

ساختار doctest python
آموزش doctest python و ساختار آن

نکته: برای اینکه در طول اجرای مثال این مقاله آموزشی با ما همراه باشید، یک Python interactive shell را در سیستم لوکال خود؛ با اجرای دستور Python3؛ باز کنید. حال با اضافه کردن مثال‌ها بعد از <<< می‌توانید آنها را کپی، پیست ویا ادیت کنید.

در ادامه یک مثال ریاضی از Doctest برای تابعی مانند add(a,b) را در نظر می‌گیریم که دو عدد را با هم جمع می‌کند:

"""
Given two integers, return the sum.

>>> add (2, 3)
5
"""

این مثال شامل یک خط توضیح(explanation)، یک مثال از تابع ()add با دو عدد صحیح بعنوان مقادیر ورودی(input) است. اگر قصد دارید در آینده بیش از 2 عدد صحیح را بوسیله این تابع جمع کنید، باید Doctest را با توجه به ورودی‌های تابع خود تغییر دهید.

داکتست فعلی تا بدین لحظه کاملا برای هر فردی قابل خواندن می‌باشد. اما می‌توان این docstring را با پارامترهای machine-readable و یک return description -برای روشن‌سازی متغیرهای ورودی/خروجی به این تابع- تکرار کرد.

در ادامه، docstringهایی را برای دو آرگومان ارسال شده به تابع و مقدار برگشتی(returned value)، اضافه می‌کنیم. داک‌استرینگ انواع دیتا را برای هر یک از valueها – پارامتر a، پارامتر b و returned value – یادداشت می‌کند. در این مورد همه پارامترها اعداد صحیح هستند.

"""
Given two integers, return the sum.

:param a: int
:param b: int
:return: int

>>> add(2, 3)
5
"""

خب اکنون Doctest آماده است تا در یک تابع قرار گیرد و تست شود.

گنجاندن Doctest در یک تابع

Doctestها معمولاً در داخل یک تابع بعد از دستور def و قبل از کدِ تابع قرار می‌گیرند. پیرو تعریف اولیه تابع، طبق قراردادهای پایتون تورفتگی‌هایی خواهد داشت. این تابع کوتاه نشان می‌دهد که یک داکتست چطور در آن گنجانده می‌شود.

def add(a, b):
"""
Given two integers, return the sum.

:param a: int
:param b: int
:return: int

>> add(2, 3)
5
"""
return a + b

اکنون یک تابع را دارید، بنابراین باید ماژول Doctest را وارد کنید و یک دستور برای اجرای آن داشته باشید.

به همین منظور باید توضیحات زیر را به قبل و بعد تابع خود اضافه کنید:

import Doctest
...
Doctest .testmod()

در این مرحله، به جای ذخیره آن در فایل برنامه، آن را روی Python shell تست کنید. برای انجام این کار می‌توانید از دستور python3 استفاده نمایید:

$ python3

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

Output
Type "help", "copyright", "credits" or "license" for more information.
>>>

پس از علامت <<< می‌توانید تایپِ کدِ مورد نظر خود را شروع کنید.

کد کامل استفاده شده در این مثال شامل تابع ()function با داکتست، داک‌استرینگ و یک فراخوان(call) برای استناد به داکتست می‌باشد. حال آن را به مترجم پایتون خود paste کنید تا آن را در بوته آزمایش قرار دهید:

import Doctest

def add(a, b):
"""
Given two integers, return the sum.

:param a: int
:param b: int
:return: int

>>> add(2, 3)
5
"""
return a + b

Doctest .testmod()

پس از اجرای کد خروجی زیر را دریافت خواهید کرد:

Output
TestResults(failed=0, attempted=1)

این بدان معنا است که کد مورد نظر بر طبق انتظارات شما اجرا شد!

دقت داشته باشید که اگر در برنامه بالا مجموع دو عدد صحیح a + b، به ضرب دو عدد a * b تغییر پیدا کند، یک اعلان از موفق‌آمیز نبودن آن دریافت خواهید کرد:

**********************************************************************
File "__main__", line 9, in __main__.add
Failed example:
    add(2, 3)
Expected:
    5
Got:
    6
**********************************************************************
1 items had failures:
   1 of   1 in __main__.add
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=1)

با استفاده از مثال بالا می‌توانید اهمیت ماژول Doctest را درک کنید. زیرا کامل برای شما نشان داده خواهد شد که چه اتفاقی رخ‌داده است. ممکن است بخواهید مثال‌های بیشتری را بررسی کنید. برای مثال مقدار a و b را صفر در نظر بگیرید و سپس مجموع آنها را با عملگر + بدست آورید:

import Doctest

def add(a, b):
"""
Given two integers, return the sum.

:param a: int
:param b: int
:return: int

>>> add(2, 3)
5
>>> add(0, 0)
0
"""
return a + b

Doctest .testmod()

پس از اجرا فیدبک زیر را از مترجم پایتون دریافت خواهید کرد:

TestResults(failed=0, attempted=2)

این خروجی نشان می‌دهد که Doctest دو تست را انجام داده است. یکی مربوط به حاصل جمع add(2,3) و دیگر مربوط به add(0,0) است. هر دو حاصل بدرستی بدست آمده‌اند.

اگر دوباره برنامه را به صورتی تغییر دهید که از عملگر ضرب * به جای عملگر + استفاده کنید، می‌توانید به اهمیت edge caseها در ماژول Doctest پی ببرید. زیرا در مثال دوم، add(0,0) مقدار مشابهی را برگشت خواهد داد( چه ضرب و چه جمع باشد).

import Doctest

def add(a, b):
"""
Given two integers, return the sum.

:param a: int
:param b: int
:return: int

>>> add(2, 3)
5
>>> add(0, 0)
0
"""
return a * b

Doctest .testmod()

اکنون خروجی پیرو برگشت داده می‌شود:

**********************************************************************
File "__main__", line 9, in __main__.add
Failed example:
add(2, 3)
Expected:
5
Got:
6
**********************************************************************
1 items had failures:
1 of 2 in __main__.add
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=2)

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

Doctest در فایل‌های برنامه نویسی

خب تاکنون از Python interactive terminal برای example خود استفاده کرده‌اید. حال از یک فایل برنامه‌نویسی برای شمارش تعداد حروف صدادار در یک کلمه استفاده کنید.

ماژول doctest python برای تست
ماژول doctest python به منظور تست راحت‌تر

در یک برنامه، می‌توانید ماژول Doctest را در کلاوز if __name__ == “__main__”: در پایین فایلِ programming ایمپورت و کال کنید. نخست یک فایل جدید — counting_vowels.py — را در تکست ادیتور خود ایجاد کنید. می‌توانید از ادیتور nano در command line استفاده نمایید:

$ nano counting_vowels.py

می‌توانید با تعریف تابع count_vowels و ارسال پارامتر word به تابع شروع کنید:

def count_vowels(word):

قبل از نوشتن بادیِ فانکشن، بهتر است درباره خواسته خود از تابع مورد نظر در Doctest توضیحاتی ارائه دهید:

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

نوع داده را با پارامتر word و نوع دیتای برگشتی را در وحله اول یک string و در حالت دوم آن را یک integer در نظر می‌گیریم. در واقع در حالت اول یک رشته و در حالت دوم یک عدد صحیح نمایش داده می‌شود.

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

برای درک بهتر موضوع می‌توانید مثالی را امتحان کنید. یک کلمه صدا دار در نظر بگیرید و آن را در رشته docstring تایپ کنید.

برای مثال کلمه ‘Cusco’ را انتخاب کنید. در این کلمه چند حرف صدادار وجود دارد؟ در زبان انگلیسی حروف صدادار شامل a، e، i، o و u است. پس در این مثال شما دو حرف صدادار o و u را دارید.

حال تست را برای کلمه Cuso اضافه می‌کنیم و برگشت آن که ۲ بعنوان تعداد اعداد صحیح در برنامه ما می‌باشد:

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

    >>> count_vowels('Cusco')
    2

برای درک بهتر موضوع مثال دیگری با تعداد حروف صدادار بیشتر مانند Manila را امتحان کنید:

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

    >>> count_vowels('Cusco')
    2

    >>> count_vowels('Manila')
    3
    """

اکنون Doctest درست کار می‌کند. بنابراین می‌توانید کدنویسی program خود را شروع کنید.

با مقداردهی اولیه یک متغیر شروع می‌کنیم: برای نگهداری حروف مصوت در یک کلمه از total_vowels استفاده کنید. اکنون در مرحله بعد حلقه for را ایجاد کنید تا در میانِ حروفِ رشتهٔ word تکرار شود. سپس از یک conditional statement برای بررسی مصوت بودنِ حروف استفاده نمایید. تعداد حروف صدادار در حین اجرای لوپ بالا خواهد رفت و در نهایت عدد آخر بدست آمده از شمارش حروف صدادارِ کلمه به total_values برگشت داده می‌شود. بنابراین برنامه مشابه ساختار ارائه شده بدون داکتست بصورت زیر می‌باشد:

def count_vowels(word):
    total_vowels = 0
    for letter in word:
        if letter in 'aeiou':
            total_vowels += 1
    return total_vowels

اکنون در پایین کدنویسی کلاوز main خود را وارد کرده و ماژول داک‌تست را اجرا کنید:

if __name__ == "__main__":
    import doctest
    doctest.testmod()

در نهایت برنامه به‌صورت زیر می‌باشد:

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

    >>> count_vowels('Cusco')
    2

    >>> count_vowels('Manila')
    3
    """
    total_vowels = 0
    for letter in word:
        if letter in 'aeiou':
            total_vowels += 1
    return total_vowels

if __name__ == "__main__":
    import doctest
    doctest.testmod()

شما می‌توانید این برنامه را با استفاده از کامندِ python یا python3(بسته به منابع مجازی شما دارد) اجرا کنید:

python counting_vowels.py

اگر تمام program شما مانند بالا نوشته شده باشد، تمامی تست ها باید موفقت‌آمیز گذرانده شوند و outputای را دریافت نمی‌کنید. این بدان معناست که تست‌ها را پاس کرده‌اید. این ویژگی برای اجرای برنامه با اهداف دیگر مفید واقع می‌شود. اگر تست را خصوصا برای دیدن نتیجه اجرا می‌کنید، می‌توانید از فلگِ v- بصورت زیر استفاده نمایید:

python counting_vowels.py -v

در نهایت با اجرای برنامه شما خروجی زیر را دریافت خواهید کرد:

Trying:
    count_vowels('Cusco')
Expecting:
    2
ok
Trying:
    count_vowels('Manila')
Expecting:
    3
ok
1 items had no tests:
    __main__
1 items passed all tests:
   2 tests in __main__.count_vowels
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

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

استفاده از Doctest برای بهبود و تقویت یک کد

تا اینجا یک برنامه داریم که کار می‌کند. اما شاید بهترین برنامه موجود نباشد. بنابراین در این مرحله سعی خواهیم کرد تا نقصان‌های جزئی آن را برطرف کنیم. بطور مثال، آیا به این موضوع فکر کرده‌اید که اگر حرف صداداری با فرم کپیتال وارد کنیم چه اتفاقی می‌افتد؟‍!

استفاده از doctests در پایتون
مثال استفاده از doctests در پایتون 

در نتیجه مثالی دیگر در Doctest وارد می‌کنیم. اینبار کلمه ‘Istanbul’ را در نظر بگیرید. مانند Manila، استامبول هم دارای سه حرف صدادار است.
در اینجا برنامه آپدیت شده با مثال جدید را می‌بینید:

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

    >>> count_vowels('Cusco')
    2

    >>> count_vowels('Manila')
    3

    >>> count_vowels('Istanbul')
    3
    """
    total_vowels = 0
    for letter in word:
        if letter in 'aeiou':
            total_vowels += 1
    return total_vowels

if __name__ == "__main__":
    import doctest
    doctest.testmod()

اکنون دوباره برنامه را اجرا کنید:

$ python counting_vowels.py

در این مرحله باگ ریزی که دنبال آن بوده‌ایم را پیدا کرده‌ایم! بعبارتی ساده‌تر، پس از اجرا خروجی زیر را دریافت خواهید کرد:

**********************************************************************
File "counting_vowels.py", line 14, in __main__.count_vowels
Failed example:
    count_vowels('Istanbul')
Expected:
    3
Got:
    2
**********************************************************************
1 items had failures:
   1 of   3 in __main__.count_vowels
***Test Failed*** 1 failures.

خروجی بالا نشان می‌دهد که برنامه شما با خطا روبرو شده است. شما انتظار داشتید که 3 حرف مصوت را نشان دهد، در حالی که تنها 2 حرف مصوت را شناسایی کرده است. اما دلیل این خطا چیست؟

دلیل خطا این است که در عبارت شرطیِ if letter in ‘aeiou’: شما حروف مصوت را بصورت کوچک آنها تعریف کرده‌اید و برنامه قادر به شناسایی حروف بزرگ نیست.

برای رفع این مشکل می‌توانید حروف مصوت را بصورت AEIOUaeiou’ تعریف کنید یا می‌توانید word را به word.lower() تغییر دهید تا در صورت وجود هر حرف بزرگ آن را به حرف کوچک و تبدیل کند و بدین ترتیب آن را هم جز شمارش خود حساب کند.:

def count_vowels(word):
    """
    Given a single word, return the total number of vowels in that single word.

    :param word: str
    :return: int

    >>> count_vowels('Cusco')
    2

    >>> count_vowels('Manila')
    3

    >>> count_vowels('Istanbul')
    3
    """
    total_vowels = 0
    for letter in word.lower():
        if letter in 'aeiou':
            total_vowels += 1
    return total_vowels

if __name__ == "__main__":
    import doctest
    doctest.testmod()

در نهایت برنامه را با استفاده از python counting_vowels.py -v و verbose flag اجرا کنید. ممکن است این برنامه، بهترین برنامه ممکن نباشد و پس از اجرا خطاهایی در آن ظاهر شود.

برای مثال اگر کلمه ‘Sydney’ را در نظر بگیرید. در زبان انگلیسی گاهی حرف Y به عنوان حروف صدادار در نظر گرفته می‌شود. یا برای مثال اگر کلمه ‘Würzburg’ را در نظر بگیرید، حرف ‘ü’ را در انگلیسی چگونه است؟ این کلمات را باید به چه صورتی در نظر گرفت؟ از چه کد یا دستوراتی برای این کلمات استفاده خواهید کرد؟

به عنوان یک برنامه‌نویس گاهی اوقات باید تصمیم‌های پیچیده‌ای را بگیرید. در بسیاری از موارد شما دامنه کامل احتمالات را در نظر نخواهید گرفت. به همین منظور ماژول Doctest ابزار خوبی برای شروع به فکر در مورد موارد خاص و ثبت documentation اولیه می‌باشد. اما در نهایت برای ساختن برنامه‌های قوی به تست‌های با استفاده از کاربر انسانی نیاز دارید.

جمع بندی

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

سوالات متداول

1. چگونه از Doctest در پایتون استفاده کنیم؟

ساده ترین راه برای شروع استفاده از doctest این است که در هر ماژول M از کد زیر استفاده کنید:

  • if __name__ == “__main__”: import doctest doctest. testmod()
  • python M. py.
  • python M. py -v.
  • python -m doctest -v example. py.

2. روش صحیح نوشتن Doctest در پایتون چیست؟

  1. تست در پایتون با استفاده از ماژول Doctest به صورت زیر است:
  2. ماژول Doctest را وارد کنید.
  3. تابع را با docstring بنویسید. در داخل Docstring دو خط زیر را برای آزمایش همان تابع بنویسید. >>>…
  4. کد تابع را بنویسید.
  5. حالا با Doctest را فراخوانی کنید. تابع testmod(name=function_name، verbose=True) برای آزمایش.