編寫函數或類時,還可爲其編寫測試。經過測試,可肯定代碼面對各類輸入都可以按要求的那樣工做。在程序中添加新代碼時,你也能夠對其進行測試,確認它們不會破壞程序既有的行爲。
在本章中,你將學習如何使用Python模塊unittest
中的工具來測試代碼。你將學習編寫測試用例,覈實一系列輸入都將獲得預期的輸出。你將看到測試經過了是什麼樣子,測試未經過又是什麼樣子,還將知道測試未經過如何有助於改進代碼。你將學習如何測試函數和類,並將知道該爲項目編寫多少個測試。 python
"""11.1 測試函數"""
""" 下面是一個簡單的函數,它接受名和姓並返回整潔的姓名: """
"""name_function.py"""
def get_formatted_name(first, last):
"""Generate a neatly formatted full name."""
full_name = first + ' ' + last
return full_name.title()
""" 爲覈實get_formatted_name()像指望的那樣工做,咱們來編寫一個 使用這個函數的程序。程序names.py讓用戶輸入名和姓,並顯示整潔的全名: """
"""names.py"""
from name_function import get_formatted_name
print "Enter 'q' at any time to quit."
while True:
first = raw_input("\nPlease give me a first name: ")
if first == 'q':
break
last = raw_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
從上述輸出可知,合併獲得的姓名正確無誤。如今假設咱們要修改get_formatted_name()
,使其還可以處理中間名。這樣作時,咱們要確保不破壞這個函數處理只有名和姓的姓名的方式。爲此,咱們能夠在每次修改get_formatted_name()
後都進行測試:運行程序names.py,並輸入像Janis Joplin這樣的姓名,但這太煩瑣了。
所幸Python提供了一種自動測試函數輸出的高效方式。假若咱們對get_formatted_name()
進行自動測試,就能始終信心滿滿,確信給這個函數提供咱們測試過的姓名時,它都能正確地工做。 程序員
Python標準庫中的模塊unittest
提供了代碼測試工具。
單元測試用於覈實函數的某個方面沒有問題;測試用例是一組單元測試,這些單元測試一塊兒覈實函數在各類情形下的行爲都符合要求。
良好的測試用例考慮到了函數可能收到的各類輸入,包含針對全部這些情形的測試。全覆蓋式測試用例包含一整套單元測試,涵蓋了各類可能的函數使用方式。對於大型項目,要實現全覆蓋可能很難。一般,最初只要針對代碼的重要行爲編寫測試便可,等項目被普遍使用時再考慮全覆蓋。 markdown
要爲函數編寫測試用例,可先導入模塊unittest
以及要測試的函數,再建立一個繼承unittest.TestCase
的類,並編寫一系列方法(須要以test或Test開頭)對函數行爲的不一樣方面進行測試。 下面是一個只包含一個方法的測試用例,它檢查函數get_formatted_name()
在給定名和姓時可否正確地工做: app
"""test_name_function.py"""
import unittest
#from name_function import get_formatted_name
def get_formatted_name(first, last):
"""Generate a neatly formatted full name."""
full_name = first + ' ' + last
return full_name.title()
class NamesTestCase(unittest.TestCase): # 這個類必須繼承unittest.TestCase類
"""測試name_function.py"""
def test_first_last_name(self):
formatted_name = get_formatted_name('janis', 'joplin')
# ①
self.assertEqual(formatted_name, 'Janis Joplin')
if __name__ == '__main__':
# unittest.main()
unittest.main(argv=['first-arg-is-ignored'], exit=False)
. Janis Joplin ---------------------------------------------------------------------- Ran 1 test in 0.002s OK
①處,咱們使用了unittest
類最有用的功能之一:一個斷言方法。斷言方法用來覈實獲得的結果是否與指望的結果一致。函數
在這裏,咱們指望formatted_name
的值爲"Janis Joplin"
。爲檢查是否確實如此,咱們調用unittes
t的方法assertEqual()
,並向它傳遞formatted_name
和"Janis Joplin"
進行比較。代碼行unittest.main()
讓Python運行這個文件中的測試。工具
說明:單元測試
書中原代碼在本地能夠運行,可是在jupyter notebook中運行報錯」AttributeError: ‘module’ object has no attribute」,看到Stack Overflow上的問答,參考修改後能夠在jupyter notebook中運行。學習
unittest.main(argv=['first-arg-is-ignored'], exit=False)
測試
unittest.main(argv=['ignored', '-v'], exit=False)
ui
修改get_formatted_name(),使其可以處理中間名,但這樣作時,故意讓這個函數沒法正確地處理像Janis Joplin這樣只有名和姓的姓名。
下面是函數get_formatted_name()的新版本,它要求經過一個實參指定中間名:
"""name_function.py"""
def get_formatted_name(first, middle, last):
"""生成整潔的姓名"""
full_name = first + ' ' + middle + ' ' + last
return full_name.title()
運行程序test_name_function.py
"""test_name_function.py"""
import unittest
#from name_function import get_formatted_name
def get_formatted_name(first, middle, last):
"""生成整潔的姓名"""
full_name = first + ' ' + middle + ' ' + last
return full_name.title()
class NamesTestCase(unittest.TestCase): # 這個類必須繼承unittest.TestCase類
"""測試name_function.py"""
def test_first_last_name(self):
formatted_name = get_formatted_name('janis', 'joplin')
self.assertEqual(formatted_name, 'Janis Joplin')
if __name__ == '__main__':
# unittest.main()
unittest.main(argv=['first-arg-is-ignored'], exit=False)
E ====================================================================== ERROR: test_first_last_name (__main__.NamesTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-22-c6f1d3890843>", line 15, in test_first_last_name formatted_name = get_formatted_name('janis', 'joplin') TypeError: get_formatted_name() takes exactly 3 arguments (2 given) ---------------------------------------------------------------------- Ran 1 test in 0.041s FAILED (errors=1)
測試未經過時怎麼辦呢?若是你檢查的條件沒錯,測試經過了意味着函數的行爲是對的,而測試未經過意味着你編寫的新代碼有錯。所以,測試未經過時,不要修改測試,而應修復致使測試不能經過的代碼:檢查剛對函數所作的修改,找出致使函數行爲不符合預期的修改。
在這個示例中,get_formatted_name()
之前只須要兩個實參——名和姓,但如今它要求提供名、中間名和姓。就這裏而言,最佳的選擇是讓中間名變爲可選的。這樣作後,使用相似於Janis Joplin的姓名 進 行 測 試 時 , 測 試 就 會 通 過 了 , 同 時 這 個 函 數 還 能 接 受 中 間 名 。 下 面 來 修 改get_formatted_name()
,將中間名設置爲可選的,而後再次運行這個測試用例。若是經過了,咱們接着確認這個函數可以妥善地處理中間名。
將中間名設置爲可選的,可在函數定義中將形參middle移到形參列表末尾,並將其默認值指定爲一個空字符串。咱們還要添加一個if測試,以便根據是否提供了中間名相應地建立姓名:
"""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()
再次運行test_name_function.py:
"""test_name_function.py"""
import unittest
from name_function import get_formatted_name
class NamesTestCase(unittest.TestCase): # 這個類必須繼承unittest.TestCase類
"""測試name_function.py"""
def test_first_last_name(self):
formatted_name = get_formatted_name('janis', 'joplin')
print formatted_name
self.assertEqual(formatted_name, 'Janis Joplin')
if __name__ == '__main__':
# unittest.main()
unittest.main(argv=['first-arg-is-ignored'], exit=False)
. Janis Joplin ---------------------------------------------------------------------- Ran 1 test in 0.003s OK
肯定get_formatted_name()
又能正確地處理簡單的姓名後,咱們再編寫一個測試,用於測試包含中間名的姓名。爲此,咱們在NamesTestCase
類中再添加一個方法:
"""test_name_function.py"""
import unittest
from name_function import get_formatted_name
class NamesTestCase(unittest.TestCase): # 這個類必須繼承unittest.TestCase類
"""測試name_function.py"""
def test_first_last_name(self):
formatted_name = get_formatted_name('janis', 'joplin')
print formatted_name
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')
if __name__ == '__main__':
# unittest.main()
unittest.main(argv=['first-arg-is-ignored'], exit=False)
... Janis Joplin ---------------------------------------------------------------------- Ran 3 tests in 0.007s OK
咱們將這個方法命名爲test_first_last_middle_name()
。方法名必須以test_打頭,這樣它纔會在咱們運行test_name_function.py時自動運行。這個方法名清楚地指出了它測試的是get_formatted_name()
的哪一個行爲,這樣,若是該測試未經過,咱們就會立刻知道受影響的是哪一種類型的姓名。在TestCase
類中使用很長的方法名是能夠的;這些方法的名稱必須是描述性的,這才能讓你明白測試未經過時的輸出;這些方法由Python自動調用,你根本不用編寫調用它們的代碼。
爲測試函數get_formatted_name()
,咱們使用名、姓和中間名調用它,再使用assertEqual()
檢查返回的姓名是否與預期的姓名(名、中間名和姓)一致。
習題11-1 城市和國家:
編寫一個函數,它接受兩個形參:一個城市名和一個國家名。這個函數返回一個格式爲 City, Country
的字符串,如 Santiago, Chile
。將這個函數存儲在一個名爲 city_functions.py 的模塊中。
建立一個名爲 test_cities.py 的程序,對剛編寫的函數進行測試(別忘了,你須要導入模塊 unittest
以及要測試的函數)。編寫一個名爲 test_city_country()
的方法,覈實使用相似於 ‘santiago’ 和 ‘chile’ 這樣的值來調用前述函數時,獲得的字符串是正確的。
運行 test_cities.py,確認測試 test_city_country()
經過了。
"test_cities.py"
import unittest
from city_functions import city_country
class CityCountryTest(unittest.TestCase):
def test_city_country(self):
formatted_string = city_country('santiago', 'chile')
self.assertEqual(formatted_string, 'Santiago, Chile')
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
... Janis Joplin ---------------------------------------------------------------------- Ran 3 tests in 0.007s OK
習題11–2 人口數量:
修改前面的函數,使其包含第三個必不可少的形參 population
,並返回一個格式爲 City, Country – population xxx
的字符串,如 Santiago, Chile – population 5000000
。運行 test_cities.py,確認測試 test_city_country()
未經過。
修改上述函數,將形參population
設置爲可選的。再次運行 test_cities.py,確認測試 test_city_country()
又經過了。
再編寫一個名爲 test_city_country_population()
的測試,覈實可使用相似於 ‘santiago’、’chile’ 和 ‘population=5000000’ 這樣的值來調用這個函數。再次運行test_cities.py,確認測試 test_city_country_population()
經過了。
"test_cities.py"
"加一個形參population"
import unittest
from city_functions import city_country
class CityCountryTest(unittest.TestCase):
def test_city_country(self):
formatted_string = city_country('santiago', 'chile')
self.assertEqual(formatted_string, 'Santiago, Chile')
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
E ====================================================================== ERROR: test_city_country (__main__.CityCountryTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "<ipython-input-1-40099b039d57>", line 9, in test_city_country formatted_string = city_country('santiago', 'chile') TypeError: city_country() takes exactly 3 arguments (2 given) ---------------------------------------------------------------------- Ran 1 test in 0.002s FAILED (errors=1)
"test_cities.py"
"形參可選"
import unittest
from city_functions import city_country
class CityCountryTest(unittest.TestCase):
def test_city_country(self):
formatted_string = city_country('santiago', 'chile')
self.assertEqual(formatted_string, 'Santiago, Chile')
if __name__ == '__main__':
unittest.main(argv=['first_arg-is-ignored'], exit=False)
. ---------------------------------------------------------------------- Ran 1 test in 0.002s OK
"test_cities.py"
"測試人口"
import unittest
from city_functions import city_country
class CityCountryTest(unittest.TestCase):
def test_city_country(self):
formatted_string = city_country('santiago', 'chile')
self.assertEqual(formatted_string, 'Santiago, Chile')
def test_city_country_population(self):
formatted_string = city_country('santiago', 'chile', 5000000)
self.assertEqual(formatted_string, 'Santiago, Chile - population 5000000')
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
.. ---------------------------------------------------------------------- Ran 2 tests in 0.005s OK
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中 |
"""11.2.2 一個要測試的類 """
"""survey.py"""
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
"""編寫一個使用AnonymousSurvey類的程序"""
"""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 = raw_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
"""11.2.3 測試 AnonymousSurvey 類 """
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""Tests for the class AnonymousSurvey."""
def test_store_single_response(self):
"""Test that a single response is stored properly."""
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
my_survey.store_response('English')
self.assertIn('English', my_survey.responses)
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
. ---------------------------------------------------------------------- Ran 1 test in 0.001s OK
這很好,但只能收集一個答案的調查用途不大。下面來覈實用戶提供三個答案時,它們也將被妥善地存儲。爲此,咱們在TestAnonymousSurvey
中再添加一個方法:
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""Tests for the class AnonymousSurvey."""
def test_store_single_response(self):
"""Test that a single response is stored properly."""
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
my_survey.store_response('English')
self.assertIn('English', my_survey.responses)
def test_store_three_responses(self):
"""Test that three individual responses are stored properly."""
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)
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
.. ---------------------------------------------------------------------- Ran 2 tests in 0.004s OK
效果很好,但這些測試有些重複的地方。下面使用unittest
的另外一項功能來提升它們的效率。
在前面的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):
"""Tests for the class AnonymousSurvey."""
def setUp(self):
""" Create a survey and a set of responses for use in all test methods. """
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):
"""Test that a single response is stored properly."""
self.my_survey.store_response(self.responses[0])
self.assertIn(self.responses[0], self.my_survey.responses)
def test_store_three_responses(self):
"""Test that three individual responses are stored properly."""
for response in self.responses:
self.my_survey.store_response(response)
for response in self.responses:
self.assertIn(response, self.my_survey.responses)
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
.. ---------------------------------------------------------------------- Ran 2 tests in 0.003s OK
方法setUp()
作了兩件事情:建立一個調查對象;建立一個答案列表。存儲這兩樣東西的變量名包含前綴self
(即存儲在屬性中),所以可在這個類的任何地方使用。這讓兩個測試方法都更簡單,由於它們都不用建立調查對象和答案。方法test_store_single_response()
核 實 self.responses
中 的 第 一 個 答 案 ——self.responses[0]
—— 被 妥 善 地 存 儲 , 而 方 法test_store_three_response()
覈實self.responses
中的所有三個答案都被妥善地存儲。
再次運行test_survey.py時,這兩個測試也都經過了。若是要擴展AnonymousSurvey
,使其容許每位用戶輸入多個答案,這些測試將頗有用。修改代碼以接受多個答案後,可運行這些測試,確認存儲單個答案或一系列答案的行爲未受影響。
測試本身編寫的類時,方法setUp()
讓測試方法編寫起來更容易:可在setUp()
方法中建立一系列實例並設置它們的屬性,再在測試方法中直接使用這些實例。相比於在每一個測試方法中都建立實例並設置其屬性,這要容易得多。
注意:
運行測試用例時,每完成一個單元測試,Python都打印一個字符:測試經過時打印一個句點;測試引起錯誤時打印一個E;測試致使斷言失敗時打印一個F。這就是你運行測試用例時,在輸出的第一行中看到的句點和字符數量各不相同的緣由。若是測試用例包含不少單元測試,須要運行很長時間,就可經過觀察這些結果來獲悉有多少個測試經過了。
11-3 僱員:
編寫一個名爲 Employee
的類,其方法__init__()
接受名、姓和年薪,並將它們都存儲在屬性中。編寫一個名爲give_raise()
的方法,它默認將年薪增長 5000美圓,但也可以接受其餘的年薪增長量。
爲 Employee
編寫一個測試用例,其中包含兩個測試方法:test_give_default_raise()
和 test_give_custom_raise()
。使用方法 setUp()
,以避免在每一個測試方法中都建立新的僱員實例。運行這個測試用例,確認兩個測試都經過了。
class Employee():
def __init__(self,last_name, first_name, salary=10000 ):
self.last_name = last_name
self.first_name = first_name
self.salary = salary
def give_raise(self,added=5000):
self.salary += added
return added
import unittest
from EmployeeFile import Employee
class TestEmployeeRaise(unittest.TestCase):
def setUp(self):
self.test1 = Employee('Tom', 'Smith')
self.test2 = Employee('Tom', 'Smith',3000)
def test_give_default_raise(self):
self.salary1 = self.test1.give_raise()
self.assertEqual(str(self.salary1), '5000')
def test_give_custom_raise(self):
self.salary2 = self.test2.give_raise(3000)
self.assertEqual(str(self.salary2), '3000')
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
.. ---------------------------------------------------------------------- Ran 2 tests in 0.003s OK
測試是不少初學者都不熟悉的主題。做爲初學者,並不是必須爲你嘗試的全部項目編寫測試;但參與工做量較大的項目時,你應對本身編寫的函數和類的重要行爲進行測試。這樣你就可以更加肯定本身所作的工做不會破壞項目的其餘部分,你就可以爲所欲爲地改進既有代碼了。若是不當心破壞了原來的功能,你立刻就會知道,從而可以輕鬆地修復問題。相比於等到不滿意的用戶報告bug後再採起措施,在測試未經過時採起措施要容易得多。 若是你在項目中包含了初步測試,其餘程序員將更敬佩你,他們將可以更駕輕就熟地嘗試使用你編寫的代碼,也更願意與你合做開發項目。若是你要跟其餘程序員開發的項目共享代碼,就必須證實你編寫的代碼經過了既有測試,一般還須要爲你添加的新行爲編寫測試。 請經過多開展測試來熟悉代碼測試過程。對於本身編寫的函數和類,請編寫針對其重要行爲的測試,但在項目早期,不要試圖去編寫全覆蓋的測試用例,除非有充分的理由這樣作。