文章目錄
- 前言
- 1 測試函數
-
- 1.1 單元測試和測試用例
- 1.2 可通過的測試
- 1.3 不能通過的測試
- 1.4 測試未通過時怎麼辦
- 1.5 添加新測試
- 2 測試類
-
- 2.1 各種斷言方法
- 2.2 一個要測試的類
- 2.3 測試 AnonymousSurvey 類
- 2.4 方法 setUp()
- 動手試一試11-3
前言
Python初學者一枚,文章僅為個人學習記錄,便于以後檢視使用。
編寫函數或類時,還可為其編寫測試。通過測試,可确定代碼面對各種輸入都能夠按要求的那樣工作。在程式中添加新代碼時,你也可以對其進行測試,确認它們不會破壞程式既有的行為。
1 測試函數
name_function.py
def get_formatted_name(first, last):
"""Generate a neatly formatted full name"""
full_name = first + ' ' + last
return full_name.title()
1.1 單元測試和測試用例
Python标準庫中的子產品unittest提供了代碼測試工具。
單元測試用于核實函數的某個方面沒有問題;
測試用例是一組單元測試,這些單元測試一起核實函數在各種情形下的行為都符合要求。
良好的測試用例考慮到了函數可能收到的各種輸入,包含針對所有這些情形的測試。全覆寫式測試用例包含一整套單元測試,涵蓋了各種可能的函數使用方式。對于大型項目,要實作全覆寫可能很難。通常,最初隻要針對代碼的重要行為編寫測試即可,等項目被廣泛使用時再考慮全覆寫。
1.2 可通過的測試
要為函數編寫測試用例,可先導入子產品unittest以及要測試的函數,再建立一個繼承unittest.TestCase的類,并編寫一系列方法對函數行為的不同方面進行測試。
下面是一個隻包含一個方法的測試用例,它檢查函數get_formatted_name()在給定名和姓時能否正确地工作:
test_name_ function.py
import unittest
from name_function import get_formatted_name
class NamesTestCase(unittest.TestCase):
"""測試name_function.py"""
def test_first_last_name(self):
"""能夠正确地處理像Janis Joplin這樣的姓名嗎?"""
formatted_name = get_formatted_name('janis', 'joplin')
self.assertEqual(formatted_name, 'Janis Joplin')
# 第一種
main = NamesTestCase
# main等于建立的類的名稱,即class後的名稱
'''
第二種:unittest.main(argv=['first-arg-is-ignored'], exit=False)
第三種:unittest.main(argv=['ignored', '-v'], exit=False)
'''
▲▲▲最後行書中原文寫unittest.main(),但會報錯:
解決方法及原因第一頁最後一條回複(2020-02-07 16:51:13)、第二頁最後一條回複(2021-02-01 10:58:49)親測有效。
首先,我們導入了子產品unittest和要測試的函數get_formatted_ name()。
在第4行,我們建立了一個名為NamesTestCase的類,用于包含一系列針對get_formatted_name()的單元測試。你可随便給這個類命名,但最好讓它看起來與要測試的函數相關,并包含字樣Test。這個類必須繼承unittest.TestCase類,這樣Python才知道如何運作你編寫的測試。
NamesTestCase隻包含一個方法,用于測試get_formatted_name()的一個方面。我們将這個方法命名為test_first_last_name(),因為我們要核實的是隻有名和姓的姓名能否被正确地格式化。
我們運作test_name_function.py時,所有以test_打頭的方法都将自動運作。在這個方法中,我們調用了要測試的函數,并存儲了要測試的傳回值。在這個示例中,我們使用實參’janis’和’joplin’調用get_formatted_name(),并将結果存儲到變量formatted_name中(見第9行)。
在第10行,我們使用了unittest類最有用的功能之一:一個斷言方法。斷言方法用來核實得到的結果是否與期望的結果一緻。
在這裡,我們知道get_formatted_name()應傳回這樣的姓名,即名和姓的首字母為大寫,且它們之間有一個空格,是以我們期望formatted_name的值為Janis Joplin。為檢查是否确實如此,我們調用unittest的方法assertEqual(),并向它傳遞formatted_ name和’Janis Joplin’。
代碼行self.assertEqual(formatted_name, ‘Janis Joplin’)的意思是說:“将formatted_name的值同字元串’Janis Joplin’進行比較,如果它們相等,就萬事大吉,如果它們不相等,跟我說一聲!”
代碼行unittest.main()讓Python運作這個檔案中的測試。
書中運作test_name_function.py時,得到的輸出:
1.3 不能通過的測試
新版本函數get_formatted_name(),它要求通過一個實參指定中間名:
name_function.py
def get_formatted_name(first, middle, last):
"""Generate a neatly formatted full name"""
full_name = first + ' ' + middle + ' ' + last
return full_name.title()
運作程式test_name_function.py時,輸出如下:
最後一行按第一種方式
最後一行按第二種方式
書中原文:
1.4 測試未通過時怎麼辦
如果你檢查的條件沒錯,測試通過了意味着函數的行為是對的,而測試未通過意味着你編寫的新代碼有錯。是以,測試未通過時,不要修改測試,而應修複導緻測試不能通過的代碼:檢查剛對函數所做的修改,找出導緻函數行為不符合預期的修改。
修改name_function.py
def get_formatted_name(first, last, middle=''):
"""Generate a neatly formatted full name"""
if middle:
full_name = first + ' ' + middle + ' ' + last
else:
full_name = first + ' ' + last
return full_name.title()
1.5 添加新測試
确定get_formatted_name()又能正确地處理簡單的姓名後,我們再編寫一個測試,用于測試包含中間名的姓名。為此,我們在NamesTestCase類中再添加一個方法:
import unittest
from name_function import get_formatted_name
class NamesTestCase(unittest.TestCase):
"""測試name_function.py"""
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')
# 第一種
main = NamesTestCase
# main等于建立的類的名稱,即class後的名稱
'''
第二種:unittest.main(argv=['first-arg-is-ignored'], exit=False)
第三種:unittest.main(argv=['ignored', '-v'], exit=False)
'''
在TestCase類中使用很長的方法名是可以的;這些方法的名稱必須是描述性的,這才能讓你明白測試未通過時的輸出;這些方法由Python自動調用,你根本不用編寫調用它們的代碼。
2 測試類
2.1 各種斷言方法
Python在unittest.TestCase類中提供了很多斷言方法。斷言方法檢查你認為應該滿足的條件是否确實滿足。如果該條件确實滿足,你對程式行為的假設就得到了确認,你就可以确信其中沒有錯誤。如果你認為應該滿足的條件實際上并不滿足,Python将引發異常。
表11-1描述了6個常用的斷言方法。使用這些方法可核實傳回的值等于或不等于預期的值、傳回的值為True或False、傳回的值在清單中或不在清單中。
你隻能在繼承unittest.TestCase的類中使用這些方法。
2.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)
這個類首先存儲了一個你指定的調查問題(見第4行),并建立了一個空清單,用于存儲答案。這個類包含列印調查問題的方法(見第9行)、在答案清單中添加新答案的方法(見第13行)以及将存儲在清單中的答案都列印出來的方法(見第17行)。要建立這個類的執行個體,隻需提供一個問題即可。有了表示調查的執行個體後,就可使用show_question()來顯示其中的問題,可使用store_response()來存儲答案,并使用show_results()來顯示調查結果。
為證明AnonymousSurvey類能夠正确地工作,我們來編寫一個使用它的程式:
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':
break
my_survey.store_response(response)
# 顯示調查結果
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()
輸出:
書中survey.py中,第11行和第20行原文錯誤,為print(question)和for response in responses:
2.3 測試 AnonymousSurvey 類
下面來編寫一個測試,對AnonymousSurvey類的行為的一個方面進行驗證:如果使用者面對調查問題時隻提供了一個答案,這個答案也能被妥善地存儲。為此,我們将在這個答案被存儲後,使用方法assertIn()來核實它包含在答案清單中:
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?"
my_survey = AnonymousSurvey(question)
my_survey.store_response('English')
self.assertIn('English', my_survey.responses)
main = TestAnonymousSurvey
我們首先導入了子產品unittest以及要測試的類AnonymousSurvey。
我們将測試用例命名為TestAnonymousSurvey,它也繼承了unittest.TestCase(見第4行)。第一個測試方法驗證調查問題的單個答案被存儲後,會包含在調查結果清單中。
要測試類的行為,需要建立其執行個體。在第10行,我們使用問題"What language did you first learn to speak?"建立了一個名為my_survey的執行個體,然後使用方法store_response()存儲了單個答案English。
接下來,我們檢查English是否包含在清單my_survey.responses中,以核實這個答案是否被妥善地存儲了(見第13行)。
當我們運作test_survey.py時,測試通過了。
下面來核實使用者提供三個答案時,它們也将被妥善地存儲。為此,我們在TestAnonymousSurvey中再添加一個方法:
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""針對AnonymousSurvey類的測試"""
def test_store_single_response(self):
"""測試單個答案會被妥善地存儲"""
--snip--
def test_store_three_responses(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)
main = TestAnonymousSurvey
前述做法的效果很好,但這些測試有些重複的地方。下面使用unittest的另一項功能來提高它們的效率。
2.4 方法 setUp()
在前面的test_survey.py中,我們在每個測試方法中都建立了一個AnonymousSurvey執行個體,并在每個方法中都建立了答案。
unittest.TestCase類包含方法setUp(),讓我們隻需建立這些對象一次,并在每個測試方法中使用它們。如果你在TestCase類中包含了方法setUp(),Python将先運作它,再運作各個以test_打頭的方法。這樣,在你編寫的每個測試方法中都可使用在方法setUp()中建立的對象了。
下面使用setUp()來建立一個調查對象和一組答案,供方法test_store_single_response()和test_store_three_responses()使用:
import unittest
from survey import AnonymousSurvey
class 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_responses(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)
main = TestAnonymousSurvey
方法setUp()做了兩件事情:建立一個調查對象(見第11行);建立一個答案清單(見第12行)。存儲這兩樣東西的變量名包含字首self(即存儲在屬性中),是以可在這個類的任何地方使用。
方法test_store_three_response()核 實 self.responses 中的第一個答案 ——self.responses[0]—— 被妥善地存儲,而方法test_store_three_response()核實self.responses中的全部三個答案都被妥善地存儲。
再次運作test_survey.py時,這兩個測試也都通過了。修改代碼以接受多個答案後,可運作這些測試,确認存儲單個答案或一系列答案的行為未受影響。
測試自己編寫的類時,方法setUp()讓測試方法編寫起來更容易:可在setUp()方法中建立一系列執行個體并設定它們的屬性,再在測試方法中直接使用這些執行個體。相比于在每個測試方法中都建立執行個體并設定其屬性,這要容易得多。
注意(看看即可):運作測試用例時,每完成一個單元測試,Python都列印一個字元:測試通過時列印一個句點;測試引發錯誤時列印一個E;測試導緻斷言失敗時列印一個F。這就是你運作測試用例時,在輸出的第一行中看到的句點和字元數量各不相同的原因。如果測試用例包含很多單元測試,需要運作很長時間,就可通過觀察這些結果來獲悉有多少個測試通過了。
動手試一試11-3
雇員:編寫一個名為 Employee 的類,其方法__init__()接受名、姓和年薪,并将它們都存儲在屬性中。編寫一個名為 give_raise()的方法,它預設将年薪增加 5000美元,但也能夠接受其他的年薪增加量。
為 Employee 編寫一個測試用例,其中包含兩個測試方法:test_give_default_ raise()和 test_give_custom_raise()。使用方法 setUp(),以免在每個測試方法中都建立新的雇員執行個體。運作這個測試用例,确認兩個測試都通過了。
employee.py
class Employee():
def __init__(self, first, last, salary):
self.first = first
self.last = last
self.salary = salary
def give_raise(self, addsalary=5000):
self.salary += addsalary
test_employee.py
import unittest
from employee import Employee
class TestEmployee(unittest.TestCase):
def setUp(self):
self.formatted_default = Employee('Edward', 'Elric', 1000)
def test_give_default_raise(self):
self.formatted_default.give_raise()
self.assertEqual(self.formatted_default.salary, 6000)
def test_give_custom_raise(self):
self.formatted_default.give_raise(10000)
self.assertEqual(self.formatted_default.salary, 11000)
main = TestEmployee