0%

【學習紀錄】Unit test

某一天幫同事 code review 後發現他重改花費很多時間,於是決定寫一下 Python 的 Unit test 教學來加速團隊的開發流程。

Unit test 顧名思義就是單元測試,何謂單元?對於手機來說,接收訊號、發射訊號、螢幕顯示、喇叭播放聲音…等等都是一個工作單元,而單元測試就是確認這些單元的功能是否如預期執行,才能確保使用者購買的產品是完整的。

在進行單元測試時,要確保測試單元和其他單元獨立,否則測試的就是兩個單元的正確性或是兩個單元協作行為。就軟體測試來說,單元測試通常都是以函式或方法為單位進行,你帶入函式固定的參數並預期函式應該要有的輸出,例如傳回預期的數值、產生的檔案、預測的資料等等。

範例

今天使用 python 的 unittest,這個套件主要包含四個部分:

  1. Test case:測試的最小單元。
  2. Test fixture:執行一或多個測試前必要的預備資源,以及相關的清除資源動作。
  3. Test suite:一組測試案例、測試套件或者是兩者的組合。
  4. Test runner:負責執行測試並提供測試結果的元件。

Test case

unittest 提供了一個類別 TestCase,透過繼承它來建立新的測試案例

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
import unittest
from util import check_POST
import mail

class FlaskTestCase(unittest.TestCase):
def setUp(self):
self.args = {
'recipient':"xxxxxx@gmail.com",
'status':102,
'query':"讓子彈飛",
'file_name':"吃著火鍋唱著歌",
'file_path':"",
'start_date':"20240101",
'end_date':"20241010"
}

def tearDown(self):
self.args = None

def test_check(self):
expected = True;
result = check_POST(self.args);
self.assertEqual(expected, result);

def test_get_mail(self):
expected = True;
if mail.get_gmail()!= False:
result = True
self.assertEqual(expected, result);

每個單元測試必須定義在一個以 test 開頭的方法中。並且以 self.assertEqual(expected, result) 驗證實際的輸出結果是否符合預期。

被測式的 check()get_email() 只是簡單的函式,一個確認的型態是否正確,另一個是取得並驗證使用者的 email 是否為公司信箱。

Test fixture

可以在 TestCase 的子類別中定義 setUp()tearDown() 函式,在每個單元測試開始前,都會執行 setUp(),而在每個單元測試結束後都會執行 tearDown()

例如:某個測試環節中 setUp() 建立了一份 log 文件,測試結束後 tearDown() 會將 log 文件刪除。

Test suite

根據測試的需求,可以將不同的函式組合在一起進行測試,例如 FlaskTestCase 中有多個 test_xxx 函式,而今天我只想將 check()get_email()組合再一起進行測試:

1
2
3
suite = unittest.TestSuite()
suite.addTest(FlaskTestCase('test_check'))
suite.addTest(FlaskTestCase('test_get_mail'))

如果要套用所有 FlaskTestCase 類別中的 test 函式:

1
suite = (unittest.TestLoader().loadTestsFromTestCase(FlaskTestCase))

也可以任意組合 test case,即使在不同的類別之中:

1
2
3
4
5
6
7
suite = unittest.TestSuite()
suite.addTest(FlaskTestCase('test_check'))
suite.addTest(FlaskTestCase('test_get_mail'))

suite2 = unittest.TestSuite()
suite2.addTest(suite)
suite2.addTest(OtherTestCase('test_somethingelse'))

Test runner

可以在 code 裡面直接使用 TextTestRunner

1
2
suite = (unittest.TestLoader().loadTestsFromTestCase(FlaskTestCase))
unittest.TextTestRunner(verbosity=2).run(suite)

或透過 main 函式來執行:

1
unittest.main(verbosity=2)

結語

對於產品來說進行 Unit test 是必不可少的,然而對於部分探索性研究的專案而言,Unit test 並非必須,因為撰寫 Unit test 需要把軟體會且可能遇到的 test case 定義清楚,並盡量測試,這個步驟會花費較多時間,而這也是為何多數企業會額外招募測試工程師進行 Unit test 的撰寫及 deploy。