第十六章 測試基礎
在編譯型語言中,需要不斷重復編輯、編譯、運行的循環。
在Python中,不存在編譯階段,只有編輯和運行階段。測試就是運行程序。
先測試再編碼
極限編程先鋒引入了“測試一點點,再編寫一點點代碼”的理念。
換而言之,測試在先,編碼在后
。這也稱為測試驅動的編程。
準確的需求說明
要闡明程序的目標,可編寫需求說明,也就是描述程序必須滿足何種需求的文檔(或便條)。
測試程序就是需求說明,可幫助確保程序開發過程緊扣這些需求。
假設你要編寫一個模塊,其中只包含一個根據矩形的寬度和高度計算面積的函數。動手編寫代碼前,編寫一個單元測試,其中包含一些你知道答案的例子。
文件area.py內容如下:
def rect_area(height,width):return height*height #很顯然不對
同目錄下的test.py內容如下:
from area import rect_area
height = 3
width = 4
correct_answer = 12
answer = rect_area(height,width)
if answer == correct_answer:print('Test passed')
else:print('Test failed')'''
很顯然,輸出結果為:Test failed
接下來,你可能檢查代碼,看看問題出在什么地方,并將返回的表達式替換為height * width。
'''
做好應對變化的準備
自動化測試不僅可在你編寫程序時提供極大的幫助,還有助于在你修改代碼時避免累積錯誤,這在程序規模很大時尤其重要。
代碼覆蓋率
覆蓋率(coverage)是一個重要的測試概念。運行測試時,很可能達不到運行所有代碼的理想狀態。(實際上,最理想的情況是,使用各種可能的輸入檢查每種可能的程序狀態,但這根本不可能做到。)優秀測試套件的目標之一是確保較高的覆蓋率,為此可使用覆蓋率工具,它們測量測試期間實際運行的代碼所占的比例。
Python自帶的程序trace.py。
要確保較高的測試覆蓋率,方法之一是秉承測試驅動開發
的理念。只要能確保先編寫測試再編寫函數,就能肯定每個函數都是經過測試的。
測試四部曲
1,確定需要實現的新功能。可將其記錄下來,再為之編寫一個測試。
2,編寫實現功能的框架代碼,讓程序能夠運行(不存在語法錯誤之類的問題),但測試依然無法通過。
3, 編寫讓測試剛好能夠通過的代碼。
4,改進(重構)代碼以全面而準確地實現所需的功能,同時確保測試依然能夠成功。
測試工具
doctest
文件my_math.py
def square(x):return x * xif name == '__main__':import doctest, my_mathdoctest.testmod(my_math)
對模塊doctest中的函數testmod進行測試
python my_math.py
先顯然,并沒有什么顯示輸出
函數doctest.testmod讀取模塊中的所有文檔字符串,查找看起來像是從交互式解釋器中摘取的示例,再檢查這些示例是否反映了實際情況。
為獲得更多的輸出,可在運行腳本時指定開關-v(verbose,意為詳盡)。
python my_math.py -v
輸入如下:
Running my_math.__doc__
0 of 0 examples failed in my_math.__doc__
Running my_math.square.__doc__
Trying: square(2)
Expecting: 4
Ok
Trying: square(3)
Expecting: 9
ok
0 of 2 examples failed in my_math.square.__doc__
1 items had no tests: test
1 items passed all tests:
2 tests in my_math.square
2 tests in 2 items.
2 passed and 0 failed.
Test passed.
假設要使用Python冪運算符而不是乘法運算符,將x * x替換為x ** 2,再運行腳本對代碼進行測試。
輸出如下:
*****************************************************************
Failure in example: square(3)
from line #5 of my_math.square
Expected: 9
Got: 27
*****************************************************************
1 items had failures: 1 of 2 in my_math.square
***Test Failed***
1 failures.
unittest
doctest使用起來很容易,但unittest(基于流行的Java測試框架JUnit)更靈活、更強大。
一個使用框架unittest的簡單測試
import unittest, my_math
class ProductTestCase(unittest.TestCase):def test_integers(self):for x in range(-10, 10):for y in range(-10, 10):p = my_math.product(x, y)self.assertEqual(p, x * y, 'Integer multiplication failed')def test_floats(self):for x in range(-10, 10):for y in range(-10, 10):x = x / 10y = y / 10p = my_math.product(x, y)self.assertEqual(p, x * y, 'Float multiplication failed')if __name__ == '__main__': unittest.main()
運行這個測試腳本將引發異常,指出模塊my_math不存在。
模塊unittest區分錯誤和失敗。錯誤指的是引發了異常,而失敗是調用failUnless等方法的結果。
文件my_math.py
def product(x,y):pass#框架代碼,沒什么意思。
運行前面的測試,將出現兩條FAIL消息,輸出如下:
FF
======================================================================
FAIL: test_floats (__main__.ProductTestCase)
----------------------------------------------------------------------
Traceback (most recent call last): File "test_my_math.py", line 17, in testFloats self.assertEqual(p, x * y, 'Float multiplication failed')
AssertionError: Float multiplication failed
======================================================================
FAIL: test_integers (__main__.ProductTestCase)
----------------------------------------------------------------------
Traceback (most recent call last): File "test_my_math.py", line 9, in testIntegers self.assertEqual(p, x * y, 'Integer multiplication failed')
AssertionError: Integer multiplication failed
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=2)
開頭兩個字符,兩個F,表示兩次失敗。
接下來需要讓代碼管用。修改文件my_math.py
def product(x,y):return x * y
再次運行前面的測試,輸出如下:
..
----------------------------------------------------------------------
Ran 2 tests in 0.015s
OK
開頭的兩個句點表示測試。
再次修改函數product,即修改文件my_math.py
def product(x, y):if x == 7 and y == 9:return 'An insidious bug has surfaced!'else:return x * y
再次運行前面的測試腳本,將有一個測試失敗。輸出如下:
.F
======================================================================
FAIL: test_integers (__main__.ProductTestCase)
----------------------------------------------------------------------
Traceback (most recent call last): File "test_my_math.py", line 9, in testIntegers self.assertEqual(p, x * y, 'Integer multiplication failed')
AssertionError: Integer multiplication failed
----------------------------------------------------------------------
Ran 2 tests in 0.005s
FAILED (failures=1)
超越單元測試
兩個工具:源代碼檢查和性能分析。
源代碼檢查是一種發現代碼中常見錯誤或問題的方式(有點像靜態類型語言中編譯器的作用,但做的事情要多得多)。
性能分析指的是搞清楚程序的運行速度到底有多快。
使用PyChecker 和 PyLint 檢查源代碼
PyChecker(pychecker.sf.net)用于檢查Python源代碼的唯一工具,能夠找出諸如給函數提供的參數不對等錯誤。
標準庫中還有tabnanny,但沒那么強大,只檢查縮進是否正確。
之后出現了PyLint(pylint.org),它支持PyChecker提供的大部分功能,還有很多其他的功能,如變量名是否符合指定的命名約定、是否遵守了自己的編碼標準等。
使用Distutils來安裝,可使用如下標準命令。
python setup.py install
PyLint,也可使用pip來安裝。
使用PyChecker來檢查文件,可運行這個腳本并將文件名作為參數
pychecker file1.py file2.py ...
使用PyLint檢查文件時,需要將模塊(或包)名
作為參數:pylint module
PyChecker和PyLint都可作為模塊(分別是pychecker.checker和pylint.lint)導入
導入pychecker.checker時,它會檢查后續代碼(包括導入的模塊),并將警告打印到標準輸出。
模塊pylint.lint包含一個文檔中沒有介紹的函數Run,這個函數是供腳本pylint本身使用的。它也將警告打印出來,而不是以某種方式將其返回。
使用模塊subprocess調用外部檢查器
import unittest, my_math
from subprocess import Popen, PIPEclass ProductTestCase(unittest.TestCase):def test_with_PyChecker(self):cmd = 'pychecker', '-Q', my_math.__file__.rstrip('c')pychecker = Popen(cmd, stdout=PIPE, stderr=PIPE)self.assertEqual(pychecker.stdout.read(), '')def test_with_PyLint(self):cmd = 'pylint', '-rn', 'my_math'pylint = Popen(cmd, stdout=PIPE, stderr=PIPE)self.assertEqual(pylint.stdout.read(), '')if __name__ == '__main__': unittest.main()
對于pychecker,開關-Q(quiet,意為靜默);
對于pylint,開關-rn(其中n表示no)以關閉報告,這意味著將只顯示警告和錯誤。
命令pylint直接將模塊名作為參數
讓pychecker正確地運行,需要獲取文件名。使用了模塊my_math
的屬性__file__,并使用rstrip將文件名末尾可能包含的c刪掉(因為模塊可能存儲在.pyc文件中)
模塊my_math,文件my_math.py
__revision__ = '0.1'
def product(factor1, factor2):'The product of two numbers'return factor1 * factor2
性能分析
在編程中,不成熟的優化是萬惡之源。
如果程序的速度達不到你的要求,必須優化,就必須首先對其進行性能分析。
標準庫包含一個卓越的性能分析模塊profile,還有一個速度更快C語言版本,名為cProfile。
這個性能分析模塊使用起來很簡單,只需調用其方法run并提供一個字符串參數。
這里照樣使用了以前的文件my_math.py
import cProfile
from my_math import product
cProfile.run('product(1, 2)')
這將輸出如下信息:各個函數和方法被調用多少次以及執行它們花費了多長時間。如果通過第二個參數向run提供一個文件名(如’my_math.profile’),分析結果將保存到這個文件中。然后,就可使用模塊pstats來研究分析結果了。
import pstats
p = pstats.Stats('my_math.profile')
小結
概念 | 描述 |
---|---|
測試驅動編程 | 大致而言,測試驅動編程意味著先測試再編碼。有了測試,就能信心滿滿地修改代碼,這讓開發和維護工作更加靈活。 |
模塊doctest和unittest | 需要在Python中進行單元測試時,這些工具必不可少。模塊doctest設計用于檢查文檔字符串中的示例,但也可輕松地使用它來設計測試套件。為讓測試套件更靈活、結構化程度更高,框架unittest很有幫助。 |
PyChecker和PyLint | 這兩個工具查看源代碼并指出潛在(和實際)的問題。它們檢查代碼的方方面面——從變量名太短到永遠不會執行的代碼段。只需編寫少量的代碼,就可將它們加入測試套件,從而確保所有修改和重構都遵循了你采用的編碼標準。 |
性能分析 | 如果很在乎速度,并想對程序進行優化(僅當絕對必要時才這樣做),應首先進行性能分析:使用模塊profile或cProfile來找出代碼中的瓶頸。 |
本章介紹的新函數
函數 | 描述 |
---|---|
doctest.testmod(module) | 檢查文檔字符串中的示例(還接受很多其他的參數) |
unittest.main() | 運行當前模塊中的單元測試 |
profile.run(stmt[,filename]) | 執行語句并對其進行性能分析;可將分析結果保存到參數filename指定的文件中 |