1 - آزمودن یک تابع

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

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

برای آموختنِ چگونگیِ آزمودنِ کد، به کدی برای آزمایش نیاز داریم. اینجا تابعی ساده وجود دارد که نام و نام خانوادگی را می‌گیرد و نام کامل را به شکلی منظّم برمی‌گرداند:

1
2
3
4
def get_formatted_name(first, last):
    """Generate a neatly formatted full name."""
    full_name = f"{first} {last}"
    return full_name.title()

تابع get_formatted_name() بین نام و نام خانوادگی یک فاصله قرار می‌دهد، آن‌ها را در قالبِ یک نام کامل ترکیب می‌کند و پس از بزرگ کردن حرف‌های اوّل هر کدام، نتیجه را برمی‌گرداند. برای بررسی این که get_formatted_name() کار می‌کند یا نه برنامه‌ای می‌نویسیم که از این تابع استفاده کند. برنامه‌ی names.py به کاربران امکان ورود نام و نام خانوادگی می‌دهد و سپس نام کامل را به شکلی مرتّب به آن‌ها نشان می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from name_function import get_formatted_name

print("Enter 'q' at any time to quit.")

while True:
    first = input("\nPlease give me a first name: ")
    if first == 'q':
        break
    last = input("Please give me a last name: ")
    if last == 'q':
        break
    formatted_name = get_formatted_name(first, last)
    print(f"\tNeatly formatted name: {formatted_name}.")

این برنامه تابع get_formatted_name() را از name_function.py وارد می‌کند. کاربران می‌توانند دنباله‌ای از نام‌ها و نام‌های خانوادگی را وارد کنند و نام کامل تولیدشده را در خروجی ببینند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Enter 'q' at any time to quit.

Please give me a first name: janis
Please give me a last name: joplin
	Neatly formatted name: Janis Joplin.

Please give me a first name: bob
Please give me a last name: dylan
	Neatly formatted name: Bob Dylan.

Please give me a first name: q

می‌بینیم که نام‌های تولیدشده درست‌اند. حال فرض کنید که می‌خواهیم تابع get_formatted_name() را طوری تغییر دهیم که بتواند نام‌های میانی را هم مدیریّت کند. همچنان‌که این کار را انجام می‌دهیم می‌خواهیم مطمئن باشیم که نحوه‌ی عملکرد تابع هنگام کار کردن با نام‌هایی که تنها از نام و نام خانوادگی تشکیل شده، به هم نمی‌خورد. می‌توانیم هر بار که get_formatted_name() را تغییر دادیم، names.py را اجرا کنیم و با وارد کردن نامی مانند Janis Joplin آن را آزمایش کنیم، امّا این کار به تدریج تکراری می‌شود. خوشبختانه پایتون راهی موثّر برای خودکارسازی فرآیندِ آزمایشِ خروجیِ یک تابع ارائه می‌کند. اگر آزمودن get_formatted_name() را خودکار کنیم، می‌توانیم همیشه مطمئن باشیم که تابع ما با انواع نام‌هایی که برای آن‌ها آزمون نوشته‌ایم کار می‌کند.

آزمون 103 102 ‌های واحد و موارد آزمایشی

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

2 - یک آزمون موفّق

کمی زمان می‌برد که به روش برپایی یک مورد آزمایشی عادت کنید امّا به محض راه انداختنِ آن، افزودن آزمون‌های واحد بیشتر برای تابع‌ها سرراست خواهد بود. به منظور نوشتن یک مورد آزمایشی برای تابعی، ماژول unittest و تابعِ مورد آزمایش را وارد کنید. سپس کلاسی بسازید که از unittest.TestCase ارث می‌برد و تعدادی متد بنویسید که جنبه‌های مختلف رفتار آن تابع را می‌آزماید.

این یک مورد آزمایشی با یک متد است که بررسی می‌کند آیا تابع get_formatted_name() با دریافت یک نام و نام خانوادگی به درستی کار می‌کند یا نه:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import unittest

from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    """Tests for 'name_function.py'."""

    def test_first_last_name(self):
        """Do names like 'Janis Joplin' work?"""
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

if __name__ == '__main__':
    unittest.main()

ابتدا unittest و تابعی را که می‌خواهیم آزمایش کنیم، get_formatted_name() وارد می‌کنیم. کلاسی به نام NamesTestCase می‌سازیم که تعدادی آزمایش واحد را برای get_formatted_name() شامل می‌شود. شما می‌توانید هر نامی برای این کلاس انتخاب کنید امّا بهتر است نامی بگذارید که به تابع تحت آزمایش مربوط باشد و از واژه‌ی Test در آن استفاده کنید. کلاس باید از کلاس unittest.TestCase ارث ببرد تا پایتون بداند چگونه آزمون‌هایی را که نوشته‌اید اجرا کند.

NamesTestCase متدی دارد که یک جنبه از عملکردِ get_formatted_name() را می‌آزماید. آن را test_first_last_name() می‌نامیم زیرا در آن تنها نام‌هایی را که از نام و نام خانوادگی تشکیل شده باشند، از منظر درستیِ شکل خروجی بررسی می‌کنیم. هر متدی که با test_ شروع شده باشد، هنگام اجرای test_name_function.py به طور خودکار اجرا خواهد شد. داخل این متد آزمون، تابعی را که قرار است آزمایش شود صدا می‌کنیم. در این مثال get_formatted_name() را با نشان‌دهنده‌های 'janis' و 'joplin' صدا می‌کنیم و نتیجه‌ی آن را به formatted_name نسبت می‌دهیم.

از یکی از سودمندترین ویژگی‌های unittest بهره می‌گیریم: متد اثبات. متدهای اثبات بررسی می‌کنند که آیا نتیجه‌ای که گرفتید یا آن‌چه انتظار دارید یکی است یا نه. در این مورد چون می‌دانیم که get_formatted_name() قرار است نام کاملی با فاصله و شکلِ مناسبِ حرف‌ها برگرداند، انتظار داریم که مقدار formatted_name برابر با Janis Joplin باشد. برای آزمودن درستیِ این مورد از متد assertEqual() در unittest استفاده کرده و formatted_name و 'Janis Joplin' را به آن می‌دهیم. خطِ

1
self.assertEqual(formatted_name, 'Janis Joplin')

می‌گوید: «مقدار formatted_name را به رشته‌ی 'Janis Joplin' مقایسه کن. اگر مطابقِ انتظار، برابر بودند، همه چیز درست است. وگرنه به من اطّلاع بده.»

این فایل را به طور مستقیم اجرا می‌کنیم امّا لازم ب106 ه ذکر است که بسیاری از ابزارها یا چهارچوب‌های آزمون، فایل‌های آزمونِ شما را پیش از اجرا وارد می‌کنند. مفسّر همزمان با وارد شدنِ چنین فایلی، آن را اجرا می‌کند. بلوک if به متغّیر ویژه‌ای، __name__ نگاه می‌کند. این متغّیر هنگام اجرای برنامه تعیین می‌شود. اگر این فایل به عنوان برنامه‌ی اصلی اجرا شود، مقدار __name__ برابر با '__main__' قرار داده می‌شود. در این مورد ما unittest.main() را صدا می‌کنیم که مورد آزمایشی را اجرا می‌کند. وقتی ابزارِ آزمون این فایل را وارد می‌کند مقدار __name__ برابر با '__main__' نخواهد بود و این بلوک اجرا نمی‌شود.

وقتی test_name_function.py را اجرا می‌کنیم به این خروجی می‌رسیم:

1
2
3
4
5
6
.

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

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

خروجی نشان می‌دهد که تابع get_formatted_name() همیشه برای نام‌هایی که از نام و نام خانوادگی تشکیل شده‌اند کار می‌کند؛ مگر این که تابع را عوض کنیم. اگر get_formatted_name() را تغییر دهیم می‌توانیم آزمون را دوباره اجرا کنیم. اگر آزمون موفقیّت‌آمیز بود می‌فهمیم که تابع همچنان برای نام‌هایی مانند Janis Joplin کار می‌کند.

3 - آزمون ناموفّق

یک آزمون ناموفّق چه شکلی است؟ بیایید get_formatted_name() را تغییر دهیم تا بتواند نام‌های میانی را هم مدیرّیت کند، امّا این کار طوری انجام دهیم که تابع در صورت وارد کردن نام و نام خانوادگی به تنهایی، مانند Janis Joplin، از کار بیفتد.

این نسخه‌ی جدید get_formatted_name() است که نام میانی را هم به عنوان نشان‌دهنده می‌پذیرد:

1
2
3
4
def get_formatted_name(first, middle, last):
    """Generate a neatly formatted full name."""
    full_name = f"{first} {middle} {last}"
    return full_name.title()

این نسخه برای افرادی که نام میانی دارند کار می‌کند امّا هنگام آزمودن آن متوجّه می‌شویم که کارکرد تابع با وارد کردنِ نام افرادی که فقط نام و نام خانوادگی دارند خراب می‌شود. این بار اجرا کردن test_name_function.py این خروجی را می‌دهد:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
E
===================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_name_function.py", line 8, in test_first_last_name
    formatted_name = get_formatted_name('janis', 'joplin')
TypeError: get_formatted_name() missing 1 required positional argument: 'last'
----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

اینجا اطّلاعات زیادی وجود دارد زیرا هنگام رد شدن یک آزمون چیزهایی زیادی هست که باید بدانیم. اولّین مورد در خروجی، یک E تنهاست که نشان می‌دهد یک آزمون واحد در این مورد آزمایشی به خطا خورده است. پس از آن می‌بینیم که test_first_last_name() در NamesTestCase موجب خطا شده است. زمانی که آزمایش شما شامل چندین آزمون واحد است، دانستن این که کدام آزمون شکست خورده حیاتی است. یک traceback استاندارد می‌بینیم که می‌گوید فراخوانی تابع get_formatted_name('janis', 'joplin') دیگر کار نمی‌کند زیرا یک نشان‌وندِ مکان‌دارِ الزامی را ندارد. در نهایت می‌بینیم که یک آزمون واحد اجرا شده است. در نهایت پیامی مبنی بر شکست خوردن کلّی مورد آزمایشی می‌بینیم و این که هنگام اجرای آن یک خطا رخ داده است. این اطّلاعات در پایان خروجی پدیدار می‌شود بنابراین آن را بلافاصله می‌بینید بدون این که نیاز داشته باشید کل خروجی طولانی را بپیمایید تا بفهمید چند آزمون شکست خورده است.

پاسخ دادن به یک آزمون ناموفّق

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

در این مورد، get_formatted_name() در ابتدا تنها دو پارامتر داشت: نام و نام خانوادگی. اکنون نام، نام میانی و نام خانوادگی را نیاز دارد. افزودن پارامترِ اجباریِ نامِ میانی، رفتارِ مطلوب get_formatted_name() را مختل کرده است. اینجا بهترین گزینه اختیاری کردن نام میانی است. به محض انجام این کار آزمون ما برای نام‌هایی مانند Janis Joplin هم موفقیّت‌آمیز خواهد بود و می‌توانیم نام‌های میانی را هم بپذیریم. بیایید get_formatted_name() را طوری تغییر دهیم که در آن نام‌های میانی اختیاری باشند و سپس آزمون را دوباره اجرا کنیم. اگر موفّق باشد، در ادامه مطمئن می‌شویم که تابع، نام‌های میانی را هم به خوبی مدیریّت می‌کند.

برای اختیاری کردن نام‌های میانی پارامتر middle را به انتهای فهرست پارامترها منتقل کرده و به آن یک مقدار پیش‌فرض خالی می‌دهیم. همچنین یک آزمون if اضافه می‌کنیم که بسته به وجود داشتن یا نداشتن نام میانی، نام کامل را به درستی ایجاد می‌کند:

1
2
3
4
5
6
7
def get_formatted_name(first, last, middle=''):
    """Generate a neatly formatted full name."""
    if middle:
        full_name = f"{first} {middle} {last}"
    else:
        full_name = f"{first} {last}"
    return full_name.title()

در نسخه‌ی جدید get_formatted_name() نام میانی اختیاری است. اگر نام میانی به تابع داده شود نام کامل شامل نام، نام میانی و نام خانوادگی خواهد بود. در غیر این صورت نام کامل تنها از نام و نام خانوادگی درست خواهد شد. حال تابع باید با هر دو نوع اسم کار کند. برای دریافتنِ این که تابع همچنان با نام‌هایی مانند Janis Joplin کار می‌کند، test_name_function.py را دوباره اجرا کنید:

1
2
3
4
5
6
.

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

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

4 - افزودن آزمون‌های جدید

حال که مطمئن شدیم get_formatted_name() با نام‌های ساده هم کار می‌کند، آزمون دیگری برای افرادی می‌نویسیم که نام میانی دارند. این کار را با اضافه کردن متدی دیگر به کلاس NamesTestCase انجام می‌دهیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class NamesTestCase(unittest.TestCase):
    """Tests for 'name_function.py'."""

    def test_first_last_name(self):
        --snip--

    def test_first_last_middle_name(self):
        """Do names like 'Wolfgang Amadeus Mozart' work?"""
        formatted_name = get_formatted_name(
            'wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

if __name__ == '__main__':
    unittest.main()

نام این متد جدید را test_first_last_middle_name() می‌گذاریم. نام متد باید با test_ شروع شود تا هنگام اجرای test_name_function.py به طور خودکار اجرا شود. متد را طوری نام‌گذاری می‌کنیم که مشخّص باشد کدام رفتار get_formatted_name() را آزمایش می‌کنیم. در نتیجه اگر آزمون شکست بخورد بلافاصله می‌فهمیم که کدام نوع نام‌ها تحت تاثیر قرار گرفته‌اند. طولانی بودن نام متدها در کلاس‌های TestCase اشکالی ندارد. نام آن‌ها باید گویا باشد تا هنگام شکست خوردن آزمون خروجی برای شما معنادار باشد؛ به جز این، چون پایتون آن‌ها را به طور خودکار صدا می‌کند، هیچ‌گاه به نوشتن کدی برای صدا کردن آن‌ها نیاز نداریم.

برای آزمودن تابع get_formatted_name()، آن را با نام، نام میانی و نام خانوادگی فراخوانی می‌کنیم و پس از آن از assertEqual() استفاده می‌کنیم تا تطابق خروجی تابع و نام کاملی را که انتظار داریم، بررسی کنیم. زمانی که دوباره test_name_function.py را اجرا می‌کنیم هر دو آزمون قبول می‌شوند:

1
2
3
4
5
6
..

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

OK

بسیار خوب! اکنون می‌دانیم که تابع همچنان با نام‌هایی مانند Janis Joplin کار می‌کند و علاوه بر آن مطمئنیم که نام‌هایی چون Wolfgang Amadeus Mozart هم در عملکرد آن خللی وارد نمی‌کنند.

خودآزمایی

۱۱-۱. شهر، کشور: تابعی بنویسید که دو پارامتر دریافت کند: نام یک شهر و نام یک کشور. این تابع باید رشته‌ای به شکل City, Country برگرداند، مانند Santiago, Chile. تابع را در ماژولی به نام city_functions.py ذخیره کنید.

فایلی به نام test_cities.py برای آزمودنِ تابعی که هم‌اکنون نوشته‌اید ایجاد کنید. (به خاطر بیاورید که باید unittest و تابع خود را وارد کنید.) متدی به نام test_city_country() بنویسید که فراخوانی تابع را با مقدارهایی مثل 'santiago' و 'chile' بیازماید تا بررسی کند که رشته‌ی صحیح را تولید می‌کند یا نه. test_cities.py را اجرا کنید و مطمئن شوید که test_city_country() موفّقیّت‌آمیز باشد.

۱۱-۲. جمعیّت: تابع را تغییر دهید تا پارامتر سومی را به نام population بپذیرد. این تابع الان باید رشته‌ای به شکل City, Country – population xxx برگرداند، مانند Santiago, Chile – population 5000000. فایل test_cities.py را دوباره اجرا کنید و مطمئن شوید که این بار شکست می‌خورد.

تابع را طوری تغییر دهید که پارامتر population در آن اختیاری باشد. فایل test_cities.py را دوباره اجرا کنید و مطمئن شوید که test_city_country() دوباره قبول می‌شود.

آزمون دومی به نام test_city_country_population() بنویسید که امکان فراخوانی تابع را با مقدارهای 'santiago'، 'chile' و population=5000000 درستی‌سنجی کند. فایل test_cities.py را دوباره اجرا کنید و مطمئن شوید که آزمون جدید موفّق است.

5 - آزمودن یک کلاس

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

گستره‌ای از متدهای اثبات

پایتون در کلاس unittest.TestCase چند متد اثبات ارائه می‌کند. چنان‌که پیش‌تر اشاره شد آزمون‌های اثبات بررسی می‌کنند که آیا شرطی که در نقطه‌ی خاصّی از برنامه به درستیِ آن اعتقاد دارید، واقعاً درست است یا نه. اگر شرط، مطابقِ انتظارِ شما درست باشد فرضیّه‌ی شما در باره‌ی رفتار آن بخش از برنامه تایید می‌شود پس می‌توانید مطمئن باشید که خطایی وجود ندارد. اگر شرطی که فرض می‌کنید درست است، نادرست باشد پایتون اعلام استثنا می‌کند.

جدول ۱۱-۱ شش متد اثبات متداول را شرح می‌دهد. با این متدها می‌توانید بررسی کنید که مقدارهای بازگشتی مساوی یا مخالف مقدارهای مورد انتظار است، یا مقدارها درست یا نادرست‌اند، یا در یک لیست وجود دارد (in) یا نه (not in). از این متدها تنها در کلاسی می‌توانید استفاده کنید که از unittest.TestCase ارث برده باشد، بنابراین اجازه دهید که در بستر آزمودن یک کلاس واقعی به نحوه‌ی کاربرد یکی از این متدها، نگاهی بیندازیم.

متد کاربرد
assertEqual(a, b) درستی‌سنجی a == b
assertNotEqual(a, b) درستی‌سنجی a != b
assertTrue(x) درستی‌سنجی این که x درست است
assertFalse(x) درستی‌سنجی این که x نادرست است
assertIn(item, list) درستی‌سنجی این که item در list هست یا نه
assertNotIn(item, list) درستی‌سنجی این که item در list نیست

کلاسی برای آزمودن

آزمودن یک کلاس مشابه آزمودن یک تابع است— بیشتر کار شما به آزمودن متدهای یک کلاس معطوف می‌شود. با این حال تفاوت‌های کوچکی نیز وجود دارد بنابراین بیاید کلاسی برای آزموده‌شدن بنویسیم. کلاسی را در نظر بگیرید که به مدیریّت نظرات ناشناس کمک می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class AnonymousSurvey:
    """Collect anonymous answers to a survey question."""

    def __init__(self, question):
        """Store a question, and prepare to store responses."""
        self.question = question
        self.responses = []

    def show_question(self):
        """Show the survey question."""
        print(self.question)

    def store_response(self, new_response):
        """Store a single response to the survey."""
        self.responses.append(new_response)

    def show_results(self):
        """Show all the responses that have been given."""
        print("Survey results:")
        for response in self.responses:
            print(f"- {response}")

این کلاس با پرسشی که برای نظرسنجی ارائه می‌کنید، شروع می‌شود و شامل لیستی خالی برای ذخیره کردن پاسخ‌هاست. این کلاس متدهایی برای چاپ کردن سوال نظرسنجی، افزودن پاسخ جدید به لیست پاسخ‌ها و چاپ کردن تمام پاسخ‌های موجود در لیست دارد. برای ساختن نمونه‌ای از کلاس کافی است به آن پرسشی ارائه کنید. با وجود یک نمونه که بیانگر نظرسنجی خاصّی باشد، می‌توانید پرسش نظرسنجی را با show_question() نشان دهید، پاسخ را به کمک store_response() ذخیره کنید و نتایج را با show_results() نمایش دهید.

برای نمایش کارکرد درست AnonymousSurvey برنامه‌ای می‌نویسیم که از آن استفاده کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from survey import AnonymousSurvey

# Define a question, and make a survey.
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)

# Show the question, and store responses to the question.
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")

while True:
    response = input("Language: ")
    if response == 'q':
        break
    my_survey.store_response(response)

# Show the survey results.
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()

این برنامه پرسشی طرح می‌کند ("What language did you first learn to speak?") و یک شیء AnonymousSurvey با آن پرسش می‌سازد. برنامه show_question() را برای نمایش پرسش فراخوانی و سپس پاسخ‌ها را دریافت می‌کند. هر کدام از پاسخ‌ها به محض دریافت ذخیره می‌شوند. پس از وارد شدنِ تمام پرسش‌ها (وقتی کاربر q را برای خروج وارد می‌کند)، متد show_results() نتایج نظرسنجی را چاپ می‌کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
What language did you first learn to speak?

Enter 'q' at any time to quit.

Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q

Thank you to everyone who participated in the survey!
Survey results:
- English
- Spanish
- English
- Mandarin

این کلاس برای نظرسنجی‌های ناشناسِ ساده کارآمد است. امّا فرض کنید می‌خواهید AnonymousSurvey و ماژول دربرگیرنده‌ی آن survey را توسعه دهیم. می‌توانیم به هر کاربر اجازه‌ی وارد کردن بیش از یک پاسخ بدهیم. می‌شود متدی نوشت که تنها پاسخ‌های یکتا را فهرست کند و گزارش کند که هر پاسخ چند بار تکرار شده است. می‌توانیم کلاس دیگری برای مدیریّت نظرسنجی‌های با نام بنویسیم.

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

آزمودن کلاس AnonymousSurvey

اکنون آزمونی می‌نویسیم که یک جنبه از رفتار AnonymousSurvey را درستی‌سنجی کند. آزمونی که می‌نویسیم بررسی می‌کند که آیا پاسخِ داده‌شده به پرسش نظرسنجی، به درستی ذخیره می‌شود یا نه. از متد assertIn() استفاده می‌کنیم تا پس از ذخیره کردن، وجود پاسخ را در لیستِ پاسخ‌ها بررسی کند:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import unittest

from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymousSurvey"""

    def test_store_single_response(self):
        """Test that a single response is stored properly."""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('English')
        self.assertIn('English', my_survey.responses)

if __name__ == '__main__':
    unittest.main()

از وارد کردنِ ماژول unittest و کلاس مورد آزمایش، AnonymousSurvey، شروع می‌کنیم. نام مورد آزمایشی را TestAnonymousSurvey می‌گذاریم که مجدّداً از unittest.TestCase ارث می‌برد. اوّلین متد آزمایش بررسی می‌کند که پاسخ ارائه‌شده به پرسش نظرسنجی، پس از ذخیره شدن، در لیست پاسخ‌های نظرسنجی می‌آید یا نه. یک نام گویای خوب برای چنین متدی test_store_single_response() است. اگر این آزمون شکست بخورد از نام متدی که در خروجی آزمون می‌آید می‌فهمیم که مشکل در ذخیره کردن پاسخ‌های تکّی نظرسنجی بوده است.

برای آزمودن رفتار کلاس باید نمونه‌ای از آن بسازیم. نمونه‌ای به نام my_survey با پرسش "What language did you first learn to speak?" می‌سازیم. یک پاسخ تکّی، English، را به کمک متد store_response() ذخیره می‌کنیم. پس از آن با اثبات وجود English در لیست my_survey.responses، ذخیره شدن درست پاسخ را درستی‌سنجی می‌کنیم.

اجرای آزمون test_survey.py همراه با موفّقیّت خواهد بود:

1
2
3
4
5
6
.

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

OK

این خوب است امّا نظرسنجی زمانی سودمند است که بیش از یک پاسخ تولید کند. اکنون بررسی می‌کنیم که آیا سه پاسخ هم به درستی ذخیره می‌شود یا نه. برای چنین کاری متدی دیگر به TestAnonymousSurvey اضافه می‌کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import unittest

from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymousSurvey"""

    def test_store_single_response(self):
        --snip--

    def test_store_three_responses(self):
        """Test that three individual responses are stored properly."""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['English', 'Spanish', 'Mandarin']
        for response in responses:
            my_survey.store_response(response)
        for response in responses:
            self.assertIn(response, my_survey.responses)

if __name__ == '__main__':
    unittest.main()

متد جدید test_store_three_responses() را صدا می‌کنیم. یک شیء نظرسنجی مانند آن‌چه در test_store_single_response() دیدیم ایجاد می‌کنیم. لیستی شامل سه پاسخ مختلف درست می‌کنیم، سپس برای هر کدام store_response() را فرامی‌خوانیم. به محض ذخیره‌شدن پاسخ‌ها حلقه‌ی دیگری می‌نویسیم که وجود هر پاسخ را در my_survey.responses تایید کند.

حال test_survey.py را دوباره اجرا می‌کنیم و هر دو آزمون (برای پاسخ تکّی و پاسخ سه‌تایی) قبول می‌شود:

1
2
3
4
5
6
..

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

OK

این آزمون بسیار عالی کار می‌کند. با این حال این آزمون‌ها کمی تکراری‌اند بنابراین از ویژگی دیگر unittest برای کاراتر کردن آن‌ها استفاده می‌کنیم.

6 - متد setUp()

در test_survey.py در هر متدِ آزمون، یک نمونه از AnonymousSurvey و مجموعه‌ای از پاسخ‌های جدید را در هر متد ساختیم. کلاس unittest.TestCase متدی به نام setUp() دارد که به شما مکان می‌دهد این اشیا را یک بار بسازید و سپس از آن‌ها در هر کدام از متدهای آزمون استفاده کنید. زمانی که متد setUp() در یک کلاس TestCase می‌آورید، پایتون آن را پیش از اجرای هر متد آزمونی که با test_ شروع می‌شود، اجرا می‌کند. هر شیء‌ای پس از ساخته‌شدن در متد setUp()، در تمام متدهای آزمون در دسترس خواهد بود.

بیایید از setUp() برای ساختن یک نمونه‌ی نظرسنجی و مجموعه‌ای از پاسخ‌ها استفاده کنیم تا بتوانیم از آن‌ها در test_store_single_response() و test_store_three_responses() استفاده کنیم:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import unittest

from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    """Tests for the class AnonymousSurvey."""

    def setUp(self):
        """Create a survey and a set of responses for use in all test methods."""
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['English', 'Spanish', 'Mandarin']

    def test_store_single_response(self):
        """Test that a single response is stored properly."""
        self.my_survey.store_response(self.responses[0])
        self.assertIn(self.responses[0], self.my_survey.responses)

    def test_store_three_responses(self):
        """Test that three individual responses are stored properly."""
        for response in self.responses:
            self.my_survey.store_response(response)
        for response in self.responses:
            self.assertIn(response, self.my_survey.responses)

if __name__ == '__main__':
    unittest.main()

متد setUp() دو کار انجام می‌دهد: یک شیء نمونه‌ی نظرسنجی می‌سازد و لیستی از پاسخ‌ها ایجاد می‌کند. هر دو پیشوند self گرفته‌اند پس می‌توان از آن‌ها هر جای کلاس استفاده کرد. این کار نوشتنِ دو متد را ساده‌تر می‌کنند چون هیچ‌کدام دیگر به ساختن نمونه‌ی نظرسنجی یا لیست پاسخ‌ها نیازی ندارند. متد test_store_single_response() ذخیره شدن اوّلین پاسخ را در self.responses[0] بررسی می‌کند و test_store_three_responses() ذخیره شدن درست هر سه پاسخ موجود را در self.responses درستی‌سنجی می‌کند.

اگر test_survey.py را دوباره اجرا کنیم هر دو آزمون موفّق خواهند بود. این آزمون‌ها به ویژه زمانی سودمندند که بخواهیم AnonymousSurvey را برای رسیدگی به چند پاسخِ یک کاربر توسعه دهیم. پس از تغییر دادن کد برای دریافت چند پاسخ از یک کاربر، می‌توانید این آزمون‌ها را دوباره اجرا کنید تا مطمئن شوید که توانایی ذخیره‌سازی یک پاسخ تکّی یا مجموعه از پاسخ‌های جدا تحت‌الشعاع قرار نگرفته باشد.

وقتی کلاس‌های خود را آزمایش می‌کنید متد setUp() نوشتن متدهای آزمون را ساده‌تر می‌کند. یک مجموعه از نمونه‌ها و مشخّصه‌ها را در setUp() می‌سازید و از آن‌ها در تمام متدهای آزمون خود استفاده می‌کنید. این کار از ساختنِ چندباره‌ی این نمونه‌ها در هر متد آزمون ساده‌تر خواهد بود.

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

خودآزمایی

۱۱-۳. کارمند: کلاسی به نام Employee بنویسید. متد __init__() باید یک نام، نام خانوادگی و حقوق سالیانه دریافت کند و آن‌ها را به عنوان مشخّصه ذخیره نماید. متدی به نام give_raise() بنویسید که به طور پیش‌فرض ۵۰۰۰ دلار به حقوق سالیانه اضافه کند امّا مقادیر افزایشِ حقوقِ دیگر را هم در صورت وجود بپذیرد.

یک مورد آزمایشی برای Employee بنویسید. دو متد آزمون بنویسید، test_give_default_raise() و test_give_custom_raise(). از متد setUp() استفاده کنید تا مجبور نباشید در هر کدام از متدها یک شیء نمونه‌ی کارمند بسازید. مورد آزمایشی خود را اجرا کنید و مطمئن شوید که هر دو آزمون موفّق خواهند بود.

خلاصه

در این فصل آموختید که چگونه به کمک ابزارهای موجود در ماژول unittest برای تابع‌ها و کلاس‌های خود آزمون بنویسید. نوشتن کلاسی را که از unittest.TestCase ارث می‌برد یاد گرفتید و این که چگونه برای درستی‌سنجی رفتارهای خاصّ تابع‌ها و کلاس‌های خود آزمون بنویسید. به‌کارگیری متد setUp() را برای ساختن کارآمدتر نمونه‌ها و مشخّصه‌ها از کلاس خود آموختید تا از آن‌ها در تمام متدهای آزمون یک کلاس استفاده کنید.

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

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

سر و کلّه زدن با آزمون‌ها برای آشنا شدن با آن‌ها بخشی از فرآیند آزمون کد شماست. برای حیاتی‌ترین رفتارهای تابع‌ها و کلاس‌های خود آزمون بنویسید امّا در پروژه‌های اوّل خود دنبالِ پوشش کامل نباشید مگر این که دلیل خاصّی برای آن داشته باشید.