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

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

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

پایتون در کلاس 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 برای کاراتر کردن آن‌ها استفاده می‌کنیم.