翻譯:《實用的Python編程》08_01_Testing
目錄 | 上一節 (7.5 裝飾方法 | 下一節 (8.2 日志)
8.1 測試
多測試,少調試(Testing Rocks, Debugging Sucks)
Python 的動態性質使得測試對大多數程序而言至關重要。編譯器不會發現你的 bug,發現 bug 的唯一方式是運行代碼,并確保嘗試了所有的特性。
斷言(Assertions)
assert 語句用于程序的內部檢查。如果表達式不為真,則會觸發 AssertionError 異常。
assert 語句語法:
assert <expression> [, 'Diagnostic message']
示例:
assert isinstance(10, int), 'Expected int'
assert 語句不應用于檢查用戶的輸入(例如,在網頁表單輸入的數據)。 assert 語句旨在用于內部檢查或者用于不變量(invariant,始終為 True 的條件)。
契約式編程
契約式編程(contract programming)也稱為契約式設計(Design By Contract),自由使用斷言是一種軟件設計方法。契約式編程規定軟件設計人員應該為軟件組件定義精確的接口規范。
例如,你可以在所有的函數輸入中使用斷言:
def add(x, y):
assert isinstance(x, int), 'Expected int'
assert isinstance(y, int), 'Expected int'
return x + y
如果函數調用者沒有使用正確的參數,那么檢查輸入可以立即捕捉到。
>>> add(2, 3)
5
>>> add('2', '3')
Traceback (most recent call last):
...
AssertionError: Expected int
>>>
內聯測試
斷言也可以用于簡單的測試。
def add(x, y):
return x + y
assert add(2,2) == 4
這樣,你就可以將測試與代碼包含在同一模塊中。
好處:如果代碼明顯被破壞,那么嘗試導入模塊將會導致程序崩潰。
對于詳盡的測試,不推薦這樣做。這種做法更像是基本的“冒煙測試(smoke test)”。函數是否可以在所有的用例上正常工作?如果不可以,那么肯定是有問題的。
unittest 模塊
假設你有下面這樣一段代碼:
# simple.py
def add(x, y):
return x + y
現在,你想對這些代碼進行測試,請創建一個單獨的測試文件,如下所示:
# test_simple.py
import simple
import unittest
然后定義一個測試類:
# test_simple.py
import simple
import unittest
# Notice that it inherits from unittest.TestCase
class TestAdd(unittest.TestCase):
...
測試類必須繼承自unittest.TestCase。
在測試類中,定義測試方法:
# test_simple.py
import simple
import unittest
# Notice that it inherits from unittest.TestCase
class TestAdd(unittest.TestCase):
def test_simple(self):
# Test with simple integer arguments
r = simple.add(2, 2)
self.assertEqual(r, 5)
def test_str(self):
# Test with strings
r = simple.add('hello', 'world')
self.assertEqual(r, 'helloworld')
重要提示:每個方法的名稱必須以 test 開頭。
使用 unittest
unittest 中內置了一些斷言,每種斷言對不同的事情進行診斷。
# Assert that expr is True
self.assertTrue(expr)
# Assert that x == y
self.assertEqual(x,y)
# Assert that x != y
self.assertNotEqual(x,y)
# Assert that x is near y
self.assertAlmostEqual(x,y,places)
# Assert that callable(arg1,arg2,...) raises exc
self.assertRaises(exc, callable, arg1, arg2, ...)
上述列表并不是一個完整的列表,unittest 模塊還有其它斷言。
運行 unittest
要運行測試,請把代碼轉換為腳本。
# test_simple.py
...
if __name__ == '__main__':
unittest.main()
然后使用 Python 執行測試文件:
bash % python3 test_simple.py
F.
========================================================
FAIL: test_simple (__main__.TestAdd)
--------------------------------------------------------
Traceback (most recent call last):
File "testsimple.py", line 8, in test_simple
self.assertEqual(r, 5)
AssertionError: 4 != 5
--------------------------------------------------------
Ran 2 tests in 0.000s
FAILED (failures=1)
說明
高效的單元測試是一種藝術。對于大型應用而言,單元測試可能會變得非常復雜。
unittest 模塊具有大量與測試運行器(test runners),測試結果集(collection of results)以及測試其他方面相關的選項。相關詳細信息,請查閱文檔。
第三方測試工具
雖然內置 unittest 模塊的優勢是可以隨處使用——因為它是 Python 的一部分,但是許多程序員也覺得 unittest 非常繁瑣。另一個流行的的測試工具是 pytest。使用 pytest,測試文件可以簡化為以下形式:
# test_simple.py
import simple
def test_simple():
assert simple.add(2,2) == 4
def test_str():
assert simple.add('hello','world') == 'helloworld'
要運行測試,只需要輸入一個命令即可,例如:python -m pytest 。它將會發現所有的測試并運行這些測試。
除了這個示例之外,pytest 還有很多內容。如果你決定嘗試一下,通常很容易上手。
練習
在本次練習中,我們將探索使用 Python unittest 模塊的基本機制(mechanics)。
在前面的練習中,我們編寫了一個包含 Stock 類的 stock.py 文件。對于本次練習,假設我們使用的是 練習7.9 中編寫的與類型化屬性相關的代碼(譯注:typedproperty.py)。如果因為某些原因,練習 7.9 的代碼無法正常工作,你可以從 Solutions/7_9 中復制 typedproperty.py 到工作目錄中。
練習 8.1:編寫單元測試
請創建一個單獨的 test_stock.py 文件,為 Stock 編寫單元測試集。為了讓你入門,這里有一小段測試實例創建的代碼:
# test_stock.py
import unittest
import stock
class TestStock(unittest.TestCase):
def test_create(self):
s = stock.Stock('GOOG', 100, 490.1)
self.assertEqual(s.name, 'GOOG')
self.assertEqual(s.shares, 100)
self.assertEqual(s.price, 490.1)
if __name__ == '__main__':
unittest.main()
運行單元測試,你應該可以獲得一些像下面這有的輸出:
.
----------------------------------------------------------------------
Ran 1 tests in 0.000s
OK
然后,編寫其它單元測試來檢查以下各項內容:
- 確保
s.cost屬性返回正確的值(49010.0)。 - 確保
s.sell()方法正常工作。它應該相應地減小s.shares。 - 確保
s.shares屬性只能設置為整數值。
對于最后一部分,你需要檢查異常的觸發。要做到這些,一種簡單的方法是使用如下代碼:
class TestStock(unittest.TestCase):
...
def test_bad_shares(self):
s = stock.Stock('GOOG', 100, 490.1)
with self.assertRaises(TypeError):
s.shares = '100'
浙公網安備 33010602011771號