(請先看這篇文章:https://blog.csdn.net/GenuineMonster/article/details/104495419)?
? ? ? ? 也許你聽過軟件測試?編寫函數或類時,可以為其編寫對應的測試代碼。通過測試,可檢驗代碼是否能夠按照編寫意圖工作。在本博文中,將會學習如何使用Python模塊unittest中的工具來測試代碼(測試函數和類),將會學習如何編寫測試用例,編寫多少個測試代碼。
1?測試函數?
????????要學習測試,得有要測試的代碼。依據原書在此提供一個簡單的函數,它接受名和姓并返回整潔的姓名,代碼如下所示:
def get_formatted_name(first,last): # 函數將名和姓結合成姓名"""生成整潔的姓名"""full_name = first + " " + lastreturn full_name.title()
測試代碼如下:(將上述代碼單獨一個文件,名為name_function.py)
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':breaklast = input("Please give me a last name: ")if last == 'q':breakformatted_name = get_formatted_name(first,last)print("\tNeatly formatted name:" + formatted_name +". ")
?????????從上述輸出可知,合并得到的姓名正確無誤。如果修改了get_formatted_name(),即使原有功能不變,還得運行測試代碼進行測試,此時就體現出自動測試函數的好處。
1.1?單元測試和測試用例?
????????Python標準庫中的模塊unittest提供了代碼測試工具。單元測試用于核實函數的某個方面沒有問題;測試用例是一組單元測試,這些單元測試一起核實函數在各種情形下的行為都符合要求。良好的測試用例考慮到了函數可能收到的各種輸入,包含針對所有這些情形的測試。全覆蓋式測試用例包含―整套單元測試,涵蓋了各種可能的函數使用方式。
1.2?可通過的測試
????????為函數編寫測試用例,可先導人模塊unittest以及要測試的函數,再創建一個繼承unittest.TestCase的類,并編寫一系列方法對函數行為的不同方面進行測試。接下來給出一個只包含一個方法的測試用例,它將檢查函數get_formatted_name()在給定名和姓時能否正確地工作:
# 首先,導入模塊unittest和要測試的函數get_formatted_name()
import unittest
from name_function import get_formatted_name
# 創建名為NamesTestCase的類,并在類的命名時體現出類的命名原則。
# 這個類必須繼承uniunittest.TestCase類,這樣Python才知道自動運行你編寫的測試代碼。# 可以看出NamesTestCase類中只包含一個方法,因此我們核實的是只有名和姓的姓名能否被正確地格式化。
"""我們運行test_name_function.py時,所有以test打頭的方法都將自動運行。在這個方法中,我們調用了要測試的函數,并存儲了要測試的返回值
"""class NamesTestCase(unittest.TestCase):"""測試name_function.py"""def test_first_last_name(self):"""能夠正確處理像Janis Joplin這樣的姓名嗎?"""formatted_name = get_formatted_name('janis','joplin')# 我們使用了unittest類中最有用的功能之一:一個斷言方法。斷言方法是用來核實得到的結果是否與期望的結果一致# 下面這行代碼的意思是說:請將程序處理得出的結果和我指定的結果進行比對,一樣就萬事大吉;不一樣,就告訴我。self.assertEqual(formatted_name,'Janis Joplin')unittest.main()
第1行的句點表明有一個測試通過了。接下來的一行指出Python運行了一個測試,消耗的時間不到0.001s。最后的ok表明該測試用例中的所有單元測試都通過了。
1.3 不能通過的測試
????????代碼錯誤通常是導致測試未通過的根本原因,接下來我們修改get_formatted_name(),使其能夠處理中間名,但是這樣的話,此函數無法正確處理像Janis Joplin這樣只用名和姓的姓名。下面是函數get_formatted_name()的新版本,它要求通過一個實參指定中間名:(下面的代碼是更新過的)
def get_formatted_name(first,middle,last):"""生成整潔的姓名"""full_name = first + " " + middle + lastreturn full_name.title()
? ? ? ? ?上述展示的是測試未通過時的結果。因為測試未通過,所以需要說明的事情更多。其中:
1、第1行輸出只有一個字母E,它表示測試用例中有一個單元測試導致了錯誤。
2、接下來,我們看到NameTestCase中的test_first_last_name()導致了錯誤。
3、錯誤提示信息中給出標準的Traceback,它指出函數調用get_formatted_name(' janis ', ' joplin ')有問題,因為它缺少一個必不可少的位置實參。
4、“Ran 1 test in 0.000s”指的是運行了一個單元測試。最后在這句的下面還看到一條消息,指出整個測試用例都未通過,因為運行該測試用例時發生了一個錯誤。
1.4 如何處理未通過的測試
????????測試未通過意味著編寫的代碼有錯誤。因此在測試未通過時,不要修改測試,而是修復新編寫的代碼:檢查剛對函數所做的修改,找出導致函數行為不符合預期的修改。
? ? ? ? 對1.3中提到的例子而言,最佳的解決方法是將中間名變為可選項,這樣就能兼容有中間名的姓名了。對此,將代碼進行如下修改:
def get_formatted_name(first,last,middle = ''):"""生成整潔的姓名"""if middle:full_name = first + ' ' + middle + ' ' + lastelse:full_name = first + ' ' + lastreturn full_name.title()
????????上述代碼:將中間名設置為可選的,并在函數定義時,將形參middle移到形參列表的末尾,并將其默認值設定為一個空字符串。與此同時,使用一個if-else語句進行判斷,分別處理有middle和沒middle的情況。接下來我們再次運行測試代碼。
?1.5 添加新測試
確定get_formatted_name()又能正確的處理簡答的姓名后,我們再編寫一個測試,用于測試包含中間名的姓名。為此,我們在NamesTestCase類中再添加一個方法,代碼如下所示:
# 首先,導入模塊unittest和要測試的函數get_formatted_name()
import unittest
from name_function import get_formatted_name
# 創建名為NamesTestCase的類,并在類的命名時體現出類的命名原則。
# 這個類必須繼承uniunittest.TestCase類,這樣Python才知道自動運行你編寫的測試代碼。# 可以看出NamesTestCase類中只包含一個方法,因此我們核實的是只有名和姓的姓名能否被正確地格式化。
"""我們運行test_name_function.py時,所有以test打頭的方法都將自動運行。在這個方法中,我們調用了要測試的函數,并存儲了要測試的返回值
"""class NamesTestCase(unittest.TestCase):"""測試name_function.py"""def test_first_last_name(self):"""能夠正確處理像Janis Joplin這樣的姓名嗎?"""formatted_name = get_formatted_name('janis','joplin')# 我們使用了unittest類中最有用的功能之一:一個斷言方法。斷言方法是用來核實得到的結果是否與期望的結果一致# 下面這行代碼的意思是說:請將程序處理得出的結果和我指定的結果進行比對,一樣就萬事大吉;不一樣,就告訴我。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()
?我們在命名函數時,都是以test開頭,而且必須這么做。這樣它才可以在我們運行test_name_function.py時自動運行。在TestCase類中使用很長的方法名是可以的,這些方法名的名稱必須是描述性的,這才能讓你明白測試未通過時的輸出;這些方法由Python自動調用,你根本不用編寫調用他們的代碼。雖然加備注也可以起到描述性的作用,但是備注不會再錯誤時顯示到終端里。
2 測試類
????????1中介紹了編寫針對單個函數的測試,下面來編寫針對類的測試。 如果針對類的測試通過了,你就能確信對類所做的改進沒有以外的破壞其原有的行為。
?2.1 各種斷言方法
????????Python在unittest.TestCase類中提供了很多斷言的方法。前面說過,斷言方法檢查你認為應該滿足的條件是否確實滿足。如果該條件確實滿足,你對程序行為的假設就得到了確認,你就可以確信其中沒有錯誤。
? ? ? ? 下面提供6種斷言方法,可核實返回的值等于或不等于預期的值;返回的值為True或False;返回的值在列表種或不在列表中(只能在繼承unittest.TestCase的類中使用這些方法)
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
2.2 一個要測試的類
? ? ? ? 類的測試與函數的測試類似——所做的大部分工作都是測試類中方法的行為,但存在一些不同之處。
survey.py文件中的代碼:
class AnonymousSurvey():"""收集匿名調查問卷的答案"""def __init__(self,question):"""存儲一個問題,并為存儲答案做準備"""self.question = questionself.responses = []def show_question(self):"""顯示調查問卷"""print(self.question)def store_response(self,new_response):"""存儲單份調查答卷"""self.responses.append(new_response)def show_result(self):"""顯示收集到的所有答案"""print("Survey results:")for response in self.responses:print('- ' + response)
使用上述類代碼的另一段代碼:文件名為language_survey.py
from survey import AnonymousSurvey# 定義一個問題,并創建一個表示調查的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':breakmy_survey.store_response(response)# 顯示調查結果
print("\nThank you everyone who participated in the survey!")
my_survey.show_results()
這個程序定義了一個問題(“ What language did you first learn to speak? "),并使用這個問題創建了一個AnonymousSurvey對象。下面是程序運行結果:
????????AnonymousSurvey類可用于進行簡單的匿名調查。假設我們將它放在了模板survey中,并想進行改進:讓每位用戶都可以輸入多個答案;編寫一個方法,它只列出不同的答案,并指出每個答案出現了多少次;再編寫一個類,用于管理非匿名調查。
? ? ? ? 進行上述修改存在風險,可能會影響AnonymousSurvey類的當前行為。例如,允許每位用戶輸入多個答案時,可能不小心修改了處理單個答案的方式。要確認在開發這個模塊時沒有破壞既有的行為,可以編寫針對這個類的測試。
2.3 測試AnonymousSurvey類
? ? ? ? 在此小節中,我們編寫一個測試,對AnonymousSurvey類的行為進行驗證。文件名為test_survey.py。
# 導入模塊unittest以及要測試的類AnonymousSurvey。將測試用例命名為TestAnonymousSurvey,它也
# 繼承了unittest.Testcase
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):"""針對AnonymousSurvey類的測試"""def test_store_single_response(self):"""測試單個答案會被妥善地存儲"""question = "what language did you first learn to speak? "# 使用問題“ what language did you first learn to speak? ”創建了一個名為my_survey的實例,# 然后使用方法store_response()存儲了單個答案English。接下來,并使用斷言,檢測English是否包含# 在列表my_survey.responses中,以核實這個答案是否被妥善的存儲了。my_survey =AnonymousSurvey(question)my_survey.store_response('English')self.assertIn('English',my_survey.responses)unittest.main()
?
?但只能收集一個答案的調查用途不大,我們擴大為3個:
import unittest
# 導入模塊unittest以及要測試的類AnonymousSurvey。將測試用例命名為TestAnonymousSurvey,它也
# 繼承了unittest.Testcase
from survey import AnonymousSurveyclass TestAnonymousSurvey(unittest.TestCase):"""針對AnonymousSurvey類的測試"""def test_store_single_response(self):"""測試單個答案會被妥善地存儲"""question = "what language did you first learn to speak?"# 使用問題“ what language did you first learn to speak? ”創建了一個名為my_survey的實例,# 然后使用方法store_response()存儲了單個答案English。接下來,并使用斷言,檢測English是否包含# 在列表my_survey.responses中,以核實這個答案是否被妥善的存儲了。my_survey = AnonymousSurvey(question)my_survey.store_response('English')self.assertIn('English',my_survey.responses)def test_store_three_responses(self):"""測試三個答案會被妥善的存儲"""question = 'What language did you first learn to speak?'my_survey = AnonymousSurvey(question)responses = 'English','Spnish','Mandarin'for response in responses:my_survey.store_response(response)for response in responses:self.assertIn(response,my_survey.responses)unittest.main()
?前述做法的效果很好,但這些測試有冗余。下面使用unittest的另一項功能來提高他們的效率。
2.4 方法setUp()
????????在前面的test_survey.py中,在每個測試方法中都創建了一個AnonymousSurvey實例,并在每個方法中都創建了答案。unittest.TestCase類中包含方法setUp(),讓我們只需要創建這些對象一次,并在每個測試方法中使用它們。如果你在TestCase類中包含了方法setUp(),Python將先運行它,再運行各個以test_打頭的方法。這樣,在編寫的每個測試方法中都可以使用在方法setUp()中創建的對象了。
? ? ? ? 下面使用setUp()來創建一個調查對象和一組答案,供方法test_store_single_response()和test_store_three_response()使用:
import unittest
# 導入模塊unittest以及要測試的類AnonymousSurvey。將測試用例命名為TestAnonymousSurvey,它也
# 繼承了unittest.Testcase
from survey import AnonymousSurveyclass TestAnonymousSurvey(unittest.TestCase):"""針對AnonymousSurvey類的測試"""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)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。這就是運行測試用例時,在輸出的第一行中看到的句點和字符數量各不相同的原因。如果測試用例包含很多單元測試,需要運行很長時間,就可通過觀察這些結果來獲悉有多少個測試通過了。
????????在項目早期,不要試圖去編寫全覆蓋的測試用例,除非有充分的理由這樣做。