最近在研究如何測試程式碼,以python的unittest
模組為例,紀錄一下學習的心得。
何謂單元測試?
所謂寫測試就是寫程式來測程式,在重構程式碼之前是很重要的一步,因為如果有先寫測試,重構完就不用debug到死,直接跑測試通過就代表修改成功,反之則代表有地方寫錯。
unittest
模組是python中標準的 unit testing framework。相較於self-test的assert
語法,unittest
可以較明確指出實際與預期結果者間的差異。
單元測試的組成
unittest
模組主要包括四個部份:
Test case(測試案例)
: 測試的最小單元。
Test fixture(測試設備)
: 測試開始前的前置作業(setUp)以及測試結束後的善後清理工作(tearDown)。
Test suite(測試套件)
: 一組測試案例、測試套件或者是兩者的組合。
Test runner(測試執行器)
: 負責執行測試
單元測試的重點
繼承自 unittest.TestCase
以 test
開頭的方法都會被視為 test method,分別代表不同的 test case
.
表示成功,F
表示失敗(Failure),E
表示錯誤(Error)。
透過 TestCase.assert..()
來做驗證,會產生比較詳細的訊息。
測試不成功時,區分為以下兩點:
-test failure(結果與預期不符)
-test error(執行期發生錯誤)
常用assertEqual()
和assertNotEqual()
遇到字串,需要轉成unicode
1 2
| def test_convert(self): self.assertEqual(unicode('hello world'), unicode('Hello World')) //強制轉型成unicode
|
setUp() 發生錯誤時,test case 不會被執行,連帶的 tearDown() 也不會被呼叫。
tearDown() 發生錯誤時,不影響下一個 test case 的 setUp()。
單元測試程式碼
1 2 3 4 5 6 7 8 9 10
| PROJECT_DIR |-- mycalc/ | |-- calculator.py | `-- __init__.py `-- tests/ |-- __init__.py |-- functional/ `-- unit/ |-- __init__.py `-- test_calculator.py
|
mycalc
是專案名稱,底下放主要的程式碼。
- 所有有關測試的程式碼放在tests/底下,再細分子目錄為unit或functional的測試
- 每個主程式碼(ex:
calculator.py
) 都會對應一個檔名用test_
開頭的測試檔。
calculator.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Calculator: def __init__(self, a, b): self.a = a self.b = b
def plus(self): return self.a + self.b
def minus(self): return self.a - self.b def multiple(self): return self.a * self.b
def mod(self): remainder = self.a % self.b quotient = (self.a-self.b)/self.b return quotient, remainder
|
test_calculator.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import unittest from calculator import Calculator
class CalculatorTest(unittest.TestCase): # 每一個testcase開始前都會被呼叫 -> 前置作業 def setUp(self): self.cal = Calculator(3,0)
# 每一個testcase結束後會被呼叫 -> 清理善後作業 def tearDown(self): self.cal = None
def test_plus(self): expected = 3; result = self.cal.plus(); self.assertEqual(expected, result);
def test_minus(self): expected = 3; result = self.cal.minus(); self.assertEqual(expected, result); def test_multiple(self): expected = 0; result = self.cal.multiple(); self.assertEqual(expected, result); def test_mod_divided_by_zero(self): self.assertRaises(ZeroDivisionError, self.cal.mod) if __name__=='__main__': unittest.main()
|
執行結果如下:
….
—————————————————–>—————–
Ran 4 tests in 0.003s
OK
除了if __name__=='__main__'
以外,也可以在終端機執行以下程式碼。
1
| $ python -m unittest test_calculator
|
這個方法可以只執行某個 class 底下所有的 test method,或是單一個 test method。
或是在終端機執行以下程式碼,可以自動找出某個資料夾底下所有的測試(預設會找 test*.py)
1
| $ python -m unittest discover
|
此外,**根據測試的需求不同,你可能會想要將不同的測試組合在一起,形成一個suite(套裝)**。
例如,CalculatorTest
中可能有數個 test_xxx
方法,而你只想將 test_plus
與 test_minus
組裝為一個測試套件的話
1 2
| tests = ['test_plus', 'test_minus'] suite = unittest.TestSuite(map(CalculatorTest, tests))
|
或是自動載入某個 TestCase 子類別中所有 test_xxx 方法
1
| suite = unittest.TestLoader().loadTestsFromTestCase(CalculatorTest)
|
組裝成suite之後,就可以進行測試,整體程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import unittest from calculator import Calculator
class CalculatorTest(unittest.TestCase): # 每一個testcase開始前都會被呼叫 -> 前置作業 def setUp(self): self.cal = Calculator(3,0)
# 每一個testcase結束後會被呼叫 -> 清理善後作業 def tearDown(self): self.cal = None
def test_plus(self): expected = 3; result = self.cal.plus(); self.assertEqual(expected, result);
def test_minus(self): expected = 3; result = self.cal.minus(); self.assertEqual(expected, result); def test_multiple(self): expected = 0; result = self.cal.multiple(); self.assertEqual(expected, result); def test_mod_divided_by_zero(self): self.assertRaises(ZeroDivisionError, self.cal.mod) # 增加這個函式 def suite(): tests = ['test_plus', 'test_minus'] suite = unittest.TestSuite(map(CalculatorTest, tests)) return suite if __name__=='__main__': # 測試執行器 runner = unittest.TextTestRunner() runner.run(suite())
|
執行結果如下:
….
—————————————————–>—————–
Ran 2 tests in 0.001s
OK
參考