閱讀《Python編程從入門到實踐》Day14

第十一章

編寫函數或類時,還可為其編寫測試。通過測試,可確定代碼面對各種輸入都能夠按要求的那樣工作。

1、測試函數

下面是要用于測試的函數,它接受名和姓并返回整潔的姓名,存儲在文件name_function.py中:

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

函數將名和姓合并成姓名,在名和姓之間加上一個空格,并將它們的首字母都大寫,再返回結果。為核實函數是否像期望的那樣工作,下面編寫一個使用這個函數的程序:

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("\tNeatly formatted name: " + formatted_name + ".")

測試如下:

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

從測試的結果可以知道,合并得到的姓名是正確的。如果現在需要添加處理中間名的功能,就需要在保證不破壞原來功能的基礎上,添加新的功能,然后再進行測試。這樣顯得就太繁瑣了,不過Python您提供了一種自動測試函數輸出的高效方式,可對相應的函數進行自動測試。

(1)單元測試和測試用例

Python標準庫中的模塊unittest提供了代碼測試工具。單元測試用于核實函數的某個方面沒有問題;測試用例是一組單元測試,這些單元測試一起核實函數在各種情形下的的行為都符合要求。良好的測試用例考慮到了函數可能收到的各種輸入,包含針對所有這些情形的的測試。全覆蓋式測試用例包含一整套單元測試,涵蓋了各種可能的函數使用方式。對于大型項目,要實現全覆蓋可能很難。通常,最初只要針對代碼的重要行為編寫測試即可,等項目被廣泛使用時再考慮全覆蓋。

(2)可通過的測試

要為函數編寫測試用例,可先導入模塊unittest以及要測試的函數,再創建一個unittest.TestCase的類,并編寫一系列方法對函數行為的不同方面進行測試。下面是只包含一個方法的測試用例:

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    def test_first_last_name(self):
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

unittest.main()
# 輸出:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

首先,需要導入模塊unittest和要測試的函數。然后創建一個類,用于包含一系列針對被測試函數的單元測試,這個類必須繼承unittest.TestCase類,這樣Python才知道如何運行你編寫的測試。
這個類只包含了一個方法,用于核實姓名能否被正確地格式化。在運行上述文件時,所有以test_大頭的方法都將自動運行。
這里使用了unittest類中最有用的功能之一:一個斷言方法。斷言方法用來核實得到的結果是否與期望的結果一致。self.assertEqual()方法就是將第一個參數和第二個參數進行比較。
在輸出的結果中,第一行的句點表明有一個測試通過了。接下來的一行指出Python運行了一個測試,消耗的時間不到0.001秒。最后的OK表明該測試用例中的所有單元測試都通過了。

(3)不能通過的測試

我們故意只在函數中添加可以處理中間名的形參middle,而沒有修改測試用例中的實參。運行結果:

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.002s

FAILED (errors=1)

由于測試沒有通過,返回了很多信息。第一行輸出只有一個字母E,它指出測試用例中有一個單元測試導致了錯誤。然后可以看到類中的函數導致了錯誤。當測試用例包含很多單元測試時,準確知道那個測試沒通過至關重要。在往下,我們看到一個標準的traceback,它準確指出函數調用中出現了問題,因為它缺少了一個必不可少的位置實參。最后顯示運行了一個單元測試,并指出整個測試用例都沒有通過。

(4)測試未通過時怎么辦

測試未通過說明你編寫的新代碼有錯,此時,不要修改測試,而應修復導致測試不能通過的代碼:檢查剛對函數所做的修改,找出導致函數行為不符合預期的修改。
對于上述未能通過的測試,我們知道是新增的中間名參數導致的,所以可以讓中間名變為可選的,即添加默認值,然后再適當地添加if判斷語句,就可以讓測試通過了。

(5)添加新測試

下面再編寫一個測試,用于測試包含中間名的姓名。

import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    def test_first_last_name(self):
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

    def test_first_last_middle_name(self):
        formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

unittest.main()
# 輸出:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

這個新添加方法的方法名必須以test_開頭,這樣它才會在我們運行整個測試文件時自動運行。在TestCase類中使用很長的方法名是可以的;這些方法名必須具有描述性的,這才能讓你明白測試未通過時的輸出;這些方法由Python自動調用,你根本不用編寫調用它們的代碼。

2、測試類

(1)unittest Module中常用的6個斷言方法
方法 用途
assertEqual(a, b) 核實a == b
assertNotEqual(a, b) 核實a != b
assertTrue(x) 核實x為True
assertFalse(x) 核實x為False
assertIn(item, list) 核實item在list中
assertNotIn(item, list) 核實item不在list中

Python在unittest.TestCase類中提供了很多斷言方法。如果你認為應該滿足的條件實際上并不滿足,Python將引發異常。

(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)

要創建這個類的實例,只需提供一個問題即可。下面編寫一個使用它的程序:

from survey import AnonymousSurvey

question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(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)

print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()
# 輸出:
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
(3)測試AnonymousSurvey類

下面編寫一個測試:如果用戶面對調查問題時只提供一個答案,這個答案也能被妥善地存儲。

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    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

這里的測試與測試函數時類似,這里的第一個方法驗證調查問題的單個答案被存儲后,會包含在調查結果列表中。要測試類的行為,需要創建其實例。由輸出知道,測試順利通過了。
下面來核實用戶提供三個答案時,它們也將被妥善地存儲。

    def test_store_three_responses(self):
        question = "What language did you first learn to speak?"
        my_survey = AnonymousSurvey(question)
        responses = ['English', 'Spanish', 'Mandarih']
        for response in responses:
            my_survey.store_response(response)
            
        for response in responses:
            self.assertIn(response, my_survey.responses)
# 測試輸出:
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

在測試類中添加上述的方法,有結果可知,測試也順利通過了。但是這些測試有些重復的地方。下面使用unittest的另一項功能來提高它們的效率。

(4)方法setUp()

在前面的示例中,我們在每個測試的方法中都創建了一個AnonymousSurvey實例,并在每個方法中都創建了答案。unittest.TestCase類中包含了方法setUp(),Python將先運行它,再運行各個以test_開頭的方法。這樣,在你編寫的每個測試方法中都可使用在方法setUp()中創建的對象。
下面使用setUp()來創建一個調查對象和一組答案,供兩個測試方法使用:

import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
    
    def setUp(self):
        question = "What language did you first learn to speak?"
        self.my_survey = AnonymousSurvey(question)
        self.responses = ['English', 'Spanish', 'Mandarih']
    
    def test_store_single_response(self):
        self.my_survey.store_response(self.responses[0])
        self.assertIn(self.responses[0], self.my_survey.responses)

    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()

方法setUp()做了兩件事情:創建一個調查對象;創建一個答案列表。存儲這兩樣東西的變量名包含前綴self(即存儲在屬性中),因此可在這個類的任何地方使用。
測試自己編寫的類時,方法setUp()讓測試方法編寫起來更容易:可在setUp()方法中創建一系列實例并設置它們的屬性,再在測試方法中直接使用這些實例。

注意:運行測試用例時,每完成一個單元測試,Python都打印一個字符:測試通過時打印一個句點;測試引發錯誤時打印一個E;測試導致斷言失敗時打印一個F。這就是你運行測試用例時,在輸出的第一行中看到的句點和字符數量各不相同的原因。如果測試用例包含很多單元測試,需要運行很長時間,就可通過觀察這些結果來獲悉有多少個測試通過了。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容

  • 編寫函數或類時,還可為其編寫測試。通過測試,可確定代碼面對各種輸入都能夠按要求的那樣工作。在程序中添加新代碼時,你...
    Darren_Lin閱讀 5,259評論 1 5
  • 洞見SELENIUM自動化測試 寫在最前面:目前自動化測試并不屬于新鮮的事物,或者說自動化測試的各種方法論已經層出...
    厲鉚兄閱讀 6,750評論 3 47
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,782評論 18 139
  • 感賞女兒下午提醒我不要再購買韓貨,薩徳系統讓女兒憤憤不平,家國情懷,政治抱負釀造了我家小憤青,大刀闊斧地分析了一...
    利利lili閱讀 201評論 0 5
  • 今天是“雙十二”,跟風上淘寶逛逛,結果一不留神沒克制住,買了兩雙童鞋,兩個兒童水杯,兩只橡木高腳凳,和一瓶護膚水。...
    成樂閱讀 155評論 0 1