Python個人學習筆記(10)——測試程式碼
技術標籤:Python個人學習筆記單元測試python程式語言
測試程式碼
編寫函式和類時,還可以為其編寫測試.通過測試,可確定程式碼面對各種輸出都能夠按要求的那樣工作.在程式中新增新的程式碼時,你也可以對其進行測試,確定它們不會破壞程式既有的行為.程式設計師都會犯錯,因此每個程式設計師都必須經常測試其程式碼,在使用者發現問題前找出它們.
學習目標:
學習如何使用Python模組unittest中的工具來測試程式碼.
學習編寫測試用例,核實一系列輸入都將得到預期的輸出
測試函式
要測試函式,得有要測試的程式碼.然後,大家也都會執行程式,然後進行相應的操作,再根據程式做出的應答判斷程式是否正常.但不可置否,這個步驟太繁瑣了.所幸python提供了一種自動測試函式輸出的高效方式.
- 單元測試和測試用例
python標準庫中的模組unittest提供了程式碼測試工具.單元測試用於核實函式的某個方面沒有問題;測試用例是一組單元測試,這些單元測試一起核實函式在各種行為都符合要求.良好的測試用例考慮到函式可能收到的各種輸入,包含針對所有這些情形的測試.全覆蓋式測試用例包含一整套單元測試,涵蓋了各種可能的函式使用方式.對於大型專案,要實現全覆蓋可能很難.通常,最初只需要針對程式碼的重要行為編寫測試即可,等專案被廣泛使用時在考慮全覆蓋. - 可通過的測試
要為函式寫測試用例,可先匯入模組unittest以及要測試的函式,再建立一個繼承unittest.TestCase的類,並編寫一系列方法去對函式行為的不同方向進行測試.下面是一個簡單的函式:
def get_formatted_name(first,last):
"""生成整潔的姓名"""
full_name = first + ' ' + last
return full_name.title()
下面是僅包含一個方法的測試用例,用於檢查get_formtted_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')
unittest.main()
執行結果:
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
- 首先,我們匯入了模組unittest和要測試的函式get_formatted_name().
- 建立一個NamesTestCase的類,用於包含一系列針對get_formatted_name()的單元測試.可以隨便對這個類命名,但最好讓它看起來要和要測試的函式相關,幷包含Test字樣.這個類必須繼承unittest.TestCase類——這樣python才知道如何執行你編寫的測試。
- 在上面,NamesTestCase類只包含一個方法,用於測試get_formatted_name()的一個方面.
- 在下面這條程式碼,我們使用了unittest類最有用的功能之一:一個斷言方法.斷言方法是用來核實得到的結果是否與期望的結果一致.這條程式碼行的意思是說:將formatted_name值與字串’Janis Joplin’作比較,如果它們相等,則萬事大吉,如果不相等,跟我說一聲.事實上,執行結果告訴我們,程式碼的功能跟預期一致.
self.assertEqual(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程式,有:
E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
能夠正確地處理像Janis Joplin這樣的姓名嗎?
----------------------------------------------------------------------
Traceback (most recent call last):
File "d:\vscode\test.py", line 10, 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,它指出測試用例中有一個單元測試導致了錯誤.
- 接下來,我們看到NamesTestCase中的test_first_last_name導致了錯誤
- 我們還看到一個標準的traceback,它指出函式呼叫get_formatted_name(‘janis’,‘joplin’)的問題.因為它缺少了一個必不可少的位置實參
- 我們還看到了運行了一個單元測試
- 最後還看到了一條資訊,它指出整個單元測試沒能通過,因為執行該測試用例時發生了一個錯誤.這條錯誤位於輸出末端,讓你一眼就能看到,你不必為獲悉有多少測試沒通過而翻閱長長的輸出.
- 測試沒通過怎麼辦?
我們對方法get_formatted_name()進行一些修改:- 讓中間名變為可選的
- 在函式定義時,將形參middle移到形參末尾,並將其預設值設為一個空字串
- 還要新增一個if測試,以便根據是否提供了中間名相應地建立姓名
name_function.py
def get_formatted_name(first,last,middle=''):
"""生成整潔的姓名"""
if middle:
full_name = first + ' ' + middle + ' ' + last
else:
full_name = first + ' ' + last
return full_name.title()
以及在類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_middle_last_name(self):
"""能正常處理像Wolfgang Amadeus Mozart這樣的姓名嗎?"""
formatted_name = get_formatted_name('Wolfgang','Mozart','Amadeus')
self.assertEqual(formatted_name,'Wolfgang Amadeus Mozart')
unittest.main()
執行結果:
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
現在,兩個測試用例都通過了.這讓我們深信這個函式既能正確地處理像Janis Joplin這樣的姓名,也能正確到處理像Wolfgang Amadeus Mozart這樣的姓名.
測試類
在前面,我們編寫了針對單個函式的的測試,下面來編寫針對類的測試
- 各種斷言方法
python在unittest.TestCase類中提供了很多斷言方法.前面說過,斷言方法檢查你認為應該滿足的條件是否得到滿足.如果該條件確實滿足,你對程式行為的假設就得到了確認,你就可以確信其中沒有錯誤.如果你認為應該滿足的條件沒有得到滿足,Python將引發異常.
下面列表中描述了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中 |
- 測試AnonymyousSurvey類
類的測試與函式的測試相似——你所做的大部分工作都是測試類中方法的行為,但存在一些不同之處.
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類的行為的一個方面進行驗證:如果使用者面對調查問題時只提供一個答案以及提供多個答案時,程式能夠妥善將其儲存:
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)
def test_store_single_response(self):
"""測試多個個答案能否被正常儲存"""
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
responses = ['English','Chinese','English']
for response in responses:
my_survey.store_response(response)
for response in responses:
self.assertIn(response,my_survey.responses)
unittest.main()
執行結果:
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
- 首先,我們依舊是要匯入unittest模組以及AnonymousSurvey類
- 分別寫兩個測試方法對不同情形進行測試
- 最後,兩個測試都通過了
前面的做法效果很好,但這些程式碼有重複的地方,下面將解決這一問題
- 方法setUp()
在前面的測試中,我們在每個測試方法都建立了一個AnonymousSurvey例項,並在每個方法都建立了答案.unittest.TestCase類中包含方法setUp,python將先執行它,再執行其它以test_打頭的方法.這樣,在編寫的每個測試方法中都可使用在方法setUp()中建立的物件.
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','Chinese','English']
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_response(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()
執行結果:
..
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
- 方法setUp()做了兩件事:建立一個調查物件;建立一個答案列表.儲存這兩樣東西的變數包含字首self(即包含在屬性內),因此可以在類的任何地方使用.這讓兩個測試變得更簡單,因為它們都不用建立新的物件和答案.
- 測試自己編寫的類時,方法setUp()讓測試方法編寫起來更簡單:可在setUp()方法中建立一系列示例並設定它們的屬性,再在測試方法中直接使用這些例項.
注意:執行測試用例時,每完成一個單元測試,python都會列印一個字元:測試通過時列印一個句點;測試引發錯誤時列印一個E;測試導致斷言失敗時列印一個F.這就是你執行測試用例時,在輸出的第一行看到的句點和字元數量各不相同的原因.如果測試用例包含很多單元測試,需要執行很長時間,就可以通過這些結果來獲悉有多少個測試通過了.
個人從書本學到的知識,作為自己學習筆記的同時,與各位朋友分享,如有不足,請多多指教
參考文獻:《Python程式設計從入門到實踐》【美】Eric Matthes 著 袁國忠 譯