MaDi's Blog

一個紀錄自己在轉職軟體工程師路上的學習小空間

0%

[Python] Unit Testing(單元測試)

最近在研究如何測試程式碼,以python的unittest模組為例,紀錄一下學習的心得。

何謂單元測試?

所謂寫測試就是寫程式來測程式,在重構程式碼之前是很重要的一步,因為如果有先寫測試,重構完就不用debug到死,直接跑測試通過就代表修改成功,反之則代表有地方寫錯。

unittest 模組是python中標準的 unit testing framework。相較於self-test的assert語法,unittest可以較明確指出實際與預期結果者間的差異。

單元測試的組成

unittest模組主要包括四個部份:

  1. Test case(測試案例): 測試的最小單元。
  2. Test fixture(測試設備): 測試開始前的前置作業(setUp)以及測試結束後的善後清理工作(tearDown)。
  3. Test suite(測試套件): 一組測試案例、測試套件或者是兩者的組合。
  4. Test runner(測試執行器): 負責執行測試

單元測試的重點

  1. 繼承自 unittest.TestCase

  2. test 開頭的方法都會被視為 test method,分別代表不同的 test case

  3. . 表示成功,F 表示失敗(Failure),E 表示錯誤(Error)。

  4. 透過 TestCase.assert..() 來做驗證,會產生比較詳細的訊息。

  5. 測試不成功時,區分為以下兩點:
    -test failure(結果與預期不符)
    -test error(執行期發生錯誤)

  6. 常用assertEqual()assertNotEqual()

  7. 遇到字串,需要轉成unicode

1
2
def test_convert(self):
self.assertEqual(unicode('hello world'), unicode('Hello World')) //強制轉型成unicode
  1. setUp() 發生錯誤時,test case 不會被執行,連帶的 tearDown() 也不會被呼叫。

  2. 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
  1. mycalc是專案名稱,底下放主要的程式碼。
  2. 所有有關測試的程式碼放在tests/底下,再細分子目錄為unit或functional的測試
  3. 每個主程式碼(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_plustest_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

參考