Python物件導向之十二:程式碼測試

晴朗_不積跬步無以至千里發表於2020-12-10

Python物件導向之十二:程式碼測試

編寫函式或類時,還可為其編寫測試。通過測試,可確定程式碼面對各種輸入都能夠按要求的那樣工作。測試讓你信心滿滿,深信即便有更多的人使用你的程式,它也能正確地工作。在程式中新增新程式碼時,你也可以對其進行測試,確認它們不會破壞程式既有的行為。程式設計師都會犯錯,因此每個程式設計師都必須經常測試其程式碼,在使用者發現問題前找出它們。

一、測試函式

下面是一個簡單的函式,它接受名和姓並返回整潔的姓名:

def get_formatted_name(first, last):
    full_name = first + ' ' + last
    return full_name.title()

1、單元測試和測試用例

Python標準庫中的模組unittest提供了程式碼測試工具。

1、單元測試:用於核實函式的某個方面沒有問題;測試用例是一組單元測試,這些單元測試一起核實函式在各種情形下的行為都符合要求。良好的測試用例考慮到了函式可能收到的各種輸入,包含針對所有這些情形的測試。

2、全覆蓋式測試:用例包含一整套單元測試,涵蓋了各種可能的函式使用方式。對於大型專案,要實現全覆蓋可能很難。通常,最初只要針對程式碼的重要行為編寫測試即可,等專案被廣泛使用時再考慮全覆蓋。

2、可通過的測試

針對上面函式進行測試:

import unittest
class NamesTestCase(unittest.TestCase):
    def test_first_last_name(self):
        """能夠正確地處理像Janis Joplin這樣的姓名嗎?"""
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

unittest.main()

分析:
1、建立了一個名為NamesTestCase的類,用於包含一系列針對get_formatted_name()的單元測試。
2、我們知道get_formatted_name()應返回這樣的姓名,即名和姓的首字母為大寫,且它們之間有一個空格,因此我們期望formatted_name的值為Janis Joplin。為檢查是否確實如此,我們呼叫unittest的方法assertEqual(),並向它傳遞formatted_ name和’Janis Joplin’。
3、程式碼行unittest.main()讓Python執行這個檔案中的測試。

執行結果:

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

OK

分析:
1、第1行的句點表明有一個測試通過了。
2、第2行指出Python執行了一個測試,消耗的時間不到0.001秒。
3、最後的OK表明該測試用例中的所有單元測試都通過了。

3、不能通過的測試

1、修改get_formatted_name被測試的函式,新增一箇中間名:

def get_formatted_name(first,  middle, last):

    full_name = first + ' ' + middle + ' '+ last
    return full_name.title()

2、執行測試類NamesTestCase後:

E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
能夠正確地處理像Janis Joplin這樣的姓名嗎?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/Users/Administrator/PycharmProjects/pythonProject1/temp/shiyan.py", line 10, 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.001s

FAILED (errors=1)

分析:
1、第1部分輸出只有一個字母E,它指出測試用例中有一個單元測試導致了錯誤。
2、第2部分錯誤追蹤:NamesTestCase中的test_first_last_name()導致了錯誤。
3、第3部分我們看到了一個標準的traceback,它指出函式呼叫get_formatted_name(‘janis’, ‘joplin’)有問題,因為它缺少一個必不可少的位置實參。
4、第4部分執行了一個單元測試, FAILED (errors=1)指整個測試用例都未通過,因為執行該測試用例時發生了一個錯誤。

4、處理未通過的測試

1、修改get_formatted_name(),將中間名設定為可選的:

def get_formatted_name(first, last, middle=''):
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name = first + ' ' + last
    return full_name.title()

2、給NamesTestCase類再新增一個方法:

import unittest
class NamesTestCase(unittest.TestCase):

    def test_first_last_name(self):
        """能夠正確地處理像Janis Joplin這樣的姓名嗎?"""
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

    def test_first_last_middle_name(self):
        """能夠正確地處理像Wolfgang Amadeus Mozart這樣的姓名嗎?"""
        formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

unittest.main()

執行結果:

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

OK

二、測試類

很多程式中都會用到類,因此能夠證明你的類能夠正確地工作會大有裨益。如果針對類的測試通過了,你就能確信對類所做的改進沒有意外地破壞其原有的行為。

1、6個常用的斷言方法

方 法用 途
assertEqual(a, b)核實a == b
assertNotEqual(a, b)核實a != b
assertTrue(x)核實x為True
assertFalse(x) 核實x為核實x為False
assertIn(item, list)核實item在list中
assertNotIn(item, list)核實item不在list中

2、一個要測試的類

類的測試與函式的測試相似——你所做的大部分工作都是測試類中方法的行為,但存在一些
不同之處,下面來編寫一個類進行測試。來看一個幫助管理匿名調查的類:

class AnonymousSurvey():
    """收集匿名調查問卷的答案"""

    def __init__(self, question):
        """儲存一個問題,併為儲存答案做準備"""
        self.question = question
        self.responses = []

    def show_question(self):
        """顯示調查問卷"""
        print(self.question)

    def store_response(self, new_response):
        """儲存單份調查答卷"""
        self.responses.append(new_response)

    def show_results(self):
        """顯示收集到的所有答卷"""
        print("Survey results:")
        for response in self.responses:
            print('- ' + response)

3、測試 AnonymousSurvey 類

下面來編寫一個測試,對AnonymousSurvey類的行為的一個方面進行驗證:如果使用者面對調查問題時只提供了一個答案,這個答案也能被妥善地儲存。為此,我們將在這個答案被儲存後,使用方法assertIn()來核實它包含在答案列表中:

import unittest

class TestAnonmyousSurvey(unittest.TestCase):
    """針對AnonymousSurvey類的測試"""
    def test_store_single_response(self):
        """測試單個答案會被妥善地儲存"""
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        my_survey.store_response('English')
        self.assertIn('English', my_survey.responses)
        
unittest.main()

執行結果:

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

OK

分析:從執行結果上可以得知收集一個答案的測試成功了,下面來核實使用者提供三個答案時,它們也將被妥善地儲存,修改以上的測試程式碼:

class TestAnonmyousSurvey(unittest.TestCase):
    """針對AnonymousSurvey類的測試"""
    def test_store_single_response(self):
        """測試三個答案會被妥善地儲存"""
        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)

unittest.main()

執行結果:

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

OK

分析:從執行結果上可以得知收集三個答案的測試成功了

4、方法 setUp()

unittest.TestCase類包含方法setUp(),讓我們只需建立這些物件一次,並在每個測試方法中使用它們。如果你在TestCase類中包含了方法setUp(),Python將先執行它,再執行各個以test_打頭的方法。

下面使用setUp()來建立一個調查物件和一組答案,供方法test_store_single_response()和
test_store_three_responses()使用:

class TestAnonmyousSurvey(unittest.TestCase):

    def setUp(self):
        """
        建立一個調查物件和一組答案,供使用的測試方法使用
        """
        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):
        """測試單個答案會被妥善地儲存"""
        self.my_survey.store_response(self.responses[0])
        self.assertIn(self.responses[0], self.my_survey.responses)

    """針對AnonymousSurvey類的測試"""

    def test_store_three_responses(self):
        """測試三個答案會被妥善地儲存"""
        for response in self.responses:
            self.my_survey.store_response(response)
        for response in self.responses:
            self.assertIn(response, self.my_survey.responses)

unittest.main()

執行結果:

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

OK

分析:這兩個測試都通過了,如果要擴充套件AnonymousSurvey,使其允許每位使用者輸入多個答案,這些測試將很有用。修改程式碼以接受多個答案後,可執行這些測試,確認儲存單個答案或一系列答案的行為未受影響。

方法setUp()做了兩件事情:建立一個調查物件;建立一個答案列表。儲存這兩樣東西的變數名包含字首self(即儲存在屬性中),因此可在這個類的任何地方使用。這讓兩個測試方法都更簡單,因為它們都不用建立調查物件和答案。
注意:執行測試用例時,每完成一個單元測試,Python都列印一個字元:測試通過時列印一個句點;測試引發錯誤時列印一個E;測試導致斷言失敗時列印一個F。這就是你執行測試用例時,在輸出的第一行中看到的句點和字元數量各不相同的原因。如果測試用例包含很多單元測試,需要執行很長時間,就可通過觀察這些結果來獲悉有多少個測試通過了。

相關文章