第十一章
編寫函數或類時,還可為其編寫測試。通過測試,可確定代碼面對各種輸入都能夠按要求的那樣工作。
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。這就是你運行測試用例時,在輸出的第一行中看到的句點和字符數量各不相同的原因。如果測試用例包含很多單元測試,需要運行很長時間,就可通過觀察這些結果來獲悉有多少個測試通過了。