Python unittest 模組教學 part 1
Last updated on Mar 20, 2023 in Python 程式設計 - 中階 by Amo Chen ‐ 5 min read
本文是 Python unittest 模組的教學:
本篇談談 Python 的 unittest 模組。
顧名思義, unittest 就是專門做單元測試(unit test)的模組。
從 Python 2.1 開始,Python 就增加了 unittest 單元測試模組, unittest 主要提供一個單元測試的框架(framework),讓每一位利用 Python 撰寫程式的人都能輕鬆的對程式進行測試。
本篇以介紹單元測試為主,如果是要針對整體程式流程及功能進行測試,那麼推薦各位可以利用 Google 搜尋相關整合測試(integration testing)的教學文章。
使用 Python unittest 的方法很簡單,只要簡單 import
即可。如下所示:
import unittest
使用 unittest 前的必要觀念
接下來,在以實際的範例進行解說前,必須對 unittest 模組中 4 個重要的名詞有所了解,這 4 個重要的名詞是 test fixture, test case, test suite, test runner,以下個別進行解說。
Test fixture
指的是進行一項測試前所需進行的準備工作,例如要測試資料庫的讀寫時,所需要的 Test fixture 就有可能是建立資料庫連線、建立測試用的資料表等。
Test case
Test case 是最小的測試單位,每一個 Test case 只會用來測試一項很小的程式邏輯(或者函式)是否如預期般運作正常。
Test suite
Test suite 則是多個 Test case 或者 Test suite 的集合,專門用來測試需要多個單元測試同時進行的測試項目,例如測試音樂播放軟對於各種格式音訊檔案的支援度,就可能需要 mp3, midi, wav 等多種音訊格式的 Test case 一起進行測試,此種由多個 Test case 組成的測試,就可稱為 Test suite 。
Test runner
在 unittest 模組中,Test runner 負責的工作是測試的執行與測試結果的呈現。例如執行測試之後,並把結果顯示給測試者。
接下來實際撰寫一個 test case 來認識 test fixture, test case 及 test runner, 詳見範例 1:
import unittest
def compare_string(s1, s2):
if s1 == s2:
return True
return False
class MyFirstTest(unittest.TestCase):
def setUp(self):
self.greeting = "Hello"
def test_compare_string(self):
test = "HellO"
self.assertFalse(compare_string(self.greeting, test))
def test_compare_byte_string(self):
byte_string = b"\x48\x65\x6c\x6c\x6f"
self.assertTrue(compare_string(self.greeting, byte_string))
if __name__ == '__main__':
tests = unittest.TestLoader().loadTestsFromTestCase(MyFirstTest)
unittest.TextTestRunner(verbosity=2).run(tests)
範例 1 執行結果如下:
test_compare_byte_string (__main__.MyFirstTest) ... FAIL
test_compare_string (__main__.MyFirstTest) ... ok
======================================================================
FAIL: test_compare_byte_string (__main__.MyFirstTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 20, in test_compare_byte_string
self.assertTrue(compare_string(self.greeting, byte_string))
AssertionError: False is not true
----------------------------------------------------------------------
Ran 2 tests in 0.000s
FAILED (failures=1)
上述範例 1 中的 compare
函式就是我們要測試的目標,可以看到上述結果測出 Python 3 的 bytes string 不等於 string, 所以出現 FAIL: test_compare_byte_string
的測試失敗訊息。
稍微解說一下範例 1 吧,我們建立了 MyFirstTest
類別,此類別繼承 unittest.TestCase
類別,這是由於所有的 test case 都需要繼承自 unittest.TestCase
類別才能夠自動被 unittest 模組的 test runner 載入執行。
接著,把焦點轉移至 setUp
方法, setUp
方法就是上述 4 個重要觀念中的 test fixture, 在這個方法中,我們可以把一些變數初始化以供測試用。在範例 1 中,我們建立了 self.greeting
屬性做為測試 compare
函式的比較變數之用。
剩下的 test_compare_string
與 test_compare_byte_string
就是 test case, 也就是測試的最小單位, compare
必須要通過這 2 個 test case 才會被認為有通過測試。
Python 的 unittest 模組規定所有的 test case 方法的命名都必須以 test
開頭(當然,這可以變更),因此範例 1 中的 test case 也都配合以 test
做為開頭,如果沒有以 test
開頭的話,就不會被認為是 test case 而不會被執行!所以必須特別注意!
此外,以 test
開頭的測試案例無法設定執行的順序,也就是說我們無法在類別中指定先執行 test_compare_byte_string
後再執行 test_compare_string
, 一切由 unittest 模組決定執行順序(預設按照字元順序)。
單元測試的目的在於測試每 1 個最小單元的程式邏輯(可以是一個函式)是否正確,如果我們所規劃的測試需要被指定先後順序,才能夠進行測試的話,就代表受測程式應該被拆解成更小的單位以方便進行測試,當我們能夠把測試拆解成不需要指定先後次序,每 1 個單元測試都能夠獨立運作不互相干擾時,就可以算是不錯的測試。
p.s. 不互相干擾才能夠保證所有測試都能平行執行,以加速整個測試時間
此外,在範例 1 的 2 個 test case 中,我們也使用繼承自 unittest 模組的 assert
相關方法,這些方法也被稱為斷言,用來評斷測試的結果是否如同我們預期的結果,舉 self.assertTrue(compare_string(self.greeting, byte_string))
為例,代表 self.assertTrue
斷言 compare_string(self.greeting, byte_string)
ㄧ定會回傳 True
, 如果不是回傳 True
就代表測試執行失敗,這時我們就得介入查清楚是 bug 還是寫錯測試斷言。
相關的 assert
方法可以至Python 的官方文件查看。
最後範例 1 使用了 unittest.TestLoader().loadTestsFromTestCase()
函式,載入了我們所撰寫的 test case, 也就是 MyFirstTest
類別,然後使用 unittest.TextTestRunner(verbosity=2).run(tests)
,執行我們所撰寫的測試,而 TextTestRunner
類別的角色就是上述提及的 test runner。
目前為止,我們已經透過範例 1 認識了 test fixture、test runner 以及 test runner, 在進一步學習 unittest 之前,先小結一下目前所學到的關於 unittest 模組的知識。
小結
- 所有的測試類別都必須繼承
unittest.TestCase
- 測試類別中的單元測試方法必須以
test
開頭作為命名 - 測試類別中的單元測試方法無法被指定順序
Test suite
當程式隨著功能的增加而越來越龐大時,測試的項目就會理所當然隨之增加,最後我們就需要針對一些相關的測試進行分類的管理,此時,我們就需要 test suite 進行管理, test suite 的範例如下所示(範例 2 ):
import unittest
def compare(v1, v2):
if v1 == v2:
return True
return False
class MyFirstTest(unittest.TestCase):
def setUp(self):
self.greeting = "Hello"
def test_compare_string(self):
test = "HellO"
self.assertFalse(compare(self.greeting, test))
def test_compare_byte_string(self):
test = b"\x48\x65\x6c\x6c\x6f"
self.assertTrue(compare(self.greeting, test))
class MySecondTest(unittest.TestCase):
def test_compare_int_bool(self):
v1 = 1
v2 = True
self.assertTrue(compare(v1, v2))
def test_compare_type(self):
v1 = type(1)
v2 = type(True)
self.assertFalse(compare(v1, v2))
if __name__ == '__main__':
test1 = unittest.TestLoader().loadTestsFromTestCase(MyFirstTest)
test2 = unittest.TestLoader().loadTestsFromTestCase(MySecondTest)
suite = unittest.TestSuite()
suite.addTests(test1)
suite.addTests(test2)
unittest.TextTestRunner(verbosity=2).run(suite)
範例 2 執行結果如下所示:
test_compare_byte_string (__main__.MyFirstTest) ... FAIL
test_compare_string (__main__.MyFirstTest) ... ok
test_compare_int_bool (__main__.MySecondTest) ... ok
test_compare_type (__main__.MySecondTest) ... ok
======================================================================
FAIL: test_compare_byte_string (__main__.MyFirstTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 20, in test_compare_byte_string
self.assertTrue(compare(self.greeting, test))
AssertionError: False is not true
----------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (failures=1)
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
在範例 2 中以 unittest.TestSuite()
建立一個 test suite ,並以 addTests
方法把 MyFirstTest
, MySecondTest
加到 test suite 中,最後以 TextTestRunner
執行此 test suite 中的所有單元測試方法。
這就是 test suite 的使用方法。
總結
認識了 unittest 模組中的 4 個基本概念後,我們就能夠將一些常用的測試項目模組化,並且建立不同的 test suite 進行管理,除了能夠減少測試成本外,也能夠增加測試效率,避免浪費太多的時間與精力在撰寫功用類似的測試上,也能夠使得測試的程式具有可重覆使用性。
不過礙於篇幅無法詳述 unittest 模組,接下來我們會在下一篇文章做更進一步的介紹。
References
unittest — Unit testing framework