一. 什麼是 Unit Test? #
「哎啊~那個函式怎麼突然不能用了!」
『程式昨天明明還可以跑啊~!QQ』
「為什麼這個 bug 竟然是因為很久以前寫的那個檔案?」
不曉得大家有沒有遇過這樣的情況?隨著專案越來越龐大,很多原本沒注意到的小函數、方法或模組,可能因為新功能的開發或 bug 的修復,結果就變得不能用了。為了避免這種狀況,最妥當的做法就是:每當有程式更動時,都要檢查專案裡的每一個函數、每一個模組、每一個類別,是否都能正常運作。這種檢查就叫做單元測試(Unit Test)。
無論你用的是什麼程式語言,不論專案大小,其實都應該進行單元測試。你可能會想:「我的專案有好幾百個函數,難道要一個一個測試嗎?」不用擔心,幾乎所有的主流程式語言都有自己的單元測試框架。更進一步,這些測試還可以整合進自動化流程,透過持續整合(Continuous Integration, CI)和持續部署(Continuous Deployment, CD),讓程式開發更輕鬆、更穩定!
單元測試除了能降低 bug 發生率,還可以讓你在重構(refactor)程式時更有信心,也方便團隊協作和未來維護。
今天拍拍君要來介紹兩個 Python 單元測試工具:Python 內建的 unittest
以及大家常用的 pytest
。接下來我們會用簡單的例子,帶大家快速上手這兩個工具!
二. Python 程式開發的 Unit Test 教學 #
1. Python 內建的 unittest #
Python 其實從很早以前就內建了單元測試框架 unittest,用法有點像其他語言的 xUnit 系列。寫法上可能稍微正式一點,但不用額外安裝,隨手就可以用。
假設你有一個簡單的加法函數 add(a, b):
def add(a,b):
return a + b
那要怎麼用 unittest
來測試它呢?很簡單,只要繼承 unittest.TestCase
,然後寫測試方法,最後在主程式呼叫 unittest.main()
就可以了:
import unittest
class TestAdd(unittest.TestCase):
def test_add_positive(self):
self.assertEqual(add(2, 3), 5) # 確保 2+3=5
def test_add_negative(self):
self.assertEqual(add(-1, -1), -2) # 確保 (-1)+(-1)=-2
def test_add_zero(self):
self.assertEqual(add(0, 10), 10) # 確保 0+10=10
if __name__ == "__main__":
unittest.main()
你只要直接執行這個 .py
檔,unittest
會自動幫你跑所有以 test_
開頭的函數,通過會顯示 OK,有錯會直接報你哪一個測試失敗,方便又直接。
上面的例子可以測試各種簡單的函數,但是更頻繁的情況我們是我們也會想要測試 class。
假設我們有一個簡單的 class 叫 Counter,用來做加一、減一:
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
def decrement(self):
self.value -= 1
用 unittest
來測試這個 class,大致寫法如下:
import unittest
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
def decrement(self):
self.value -= 1
class TestCounter(unittest.TestCase):
def setUp(self):
# 每個測試 case 開始前都會自動執行這裡
self.counter = Counter()
def test_initial_value(self):
self.assertEqual(self.counter.value, 0)
def test_increment(self):
self.counter.increment()
self.assertEqual(self.counter.value, 1)
def test_decrement(self):
self.counter.decrement()
self.assertEqual(self.counter.value, -1)
if __name__ == "__main__":
unittest.main()
2. Pytest #
pytest
可以說是 Python 世界裡最受歡迎的第三方測試框架,寫法比 unittest
更簡潔,還有超多進階功能。不用寫 class
,只要用普通函數、直接 assert
就行,非常直觀!
首先,要先安裝 pytest
(只要做一次):
pip install pytest
然後來看看同樣的加法測試用 pytest
怎麼寫:
def add(a, b):
return a + b
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -1) == -2
def test_add_zero():
assert add(0, 10) == 10
這樣就好了!沒有 class,沒有複雜的語法,乾淨又好懂。
執行方式也很簡單,只要在終端機下:
pytest
pytest
會自動搜尋所有 test_
開頭的函數並執行,結果一目了然。而且失敗時的錯誤訊息很清楚,還可以搭配許多外掛,進階需求也都能滿足。
pytest
測試 class 其實一樣很簡單,你可以直接寫測試函數,每次測試要用到物件的時候就自己建立。
如果你想讓每個測試都自動有一個新的物件,可以用 pytest 的 fixture。
基本寫法:
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
def decrement(self):
self.value -= 1
def test_initial_value():
c = Counter()
assert c.value == 0
def test_increment():
c = Counter()
c.increment()
assert c.value == 1
def test_decrement():
c = Counter()
c.decrement()
assert c.value == -1
或是寫一個 Test*
開頭的 class:
class TestCounter:
def setup_method(self):
# 這個方法會在每個 test 開頭自動呼叫,類似 unittest 的 setUp
self.counter = Counter()
def test_initial_value(self):
assert self.counter.value == 0
def test_increment(self):
self.counter.increment()
assert self.counter.value == 1
def test_decrement(self):
self.counter.decrement()
assert self.counter.value == -1
TestCounter
這個 class 不需要繼承任何東西,pytest
會自動找到所有 Test
開頭的 class,執行裡面所有 test_
開頭的方法。
如果你要在每個測試方法前準備一個新物件,可以用 setup_method(self)
(名字一定要對,pytest
會自動呼叫)。
如果你有 teardown(測試後清理),可以加 teardown_method(self)
。
也可以用 fixture(比較進階)寫法:
import pytest
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
def decrement(self):
self.value -= 1
@pytest.fixture
def counter():
return Counter()
def test_initial_value(counter):
assert counter.value == 0
def test_increment(counter):
counter.increment()
assert counter.value == 1
def test_decrement(counter):
counter.decrement()
assert counter.value == -1
三. 小技巧 #
-
功能要小,測試才有效!
每個單元測試都應該只針對「一個」功能點或情境來驗證,測太多事情反而不容易查錯。簡單來說,就是一個測試只檢查一種結果,出錯時才容易定位。 -
命名要清楚,不怕長,只怕看不懂
測試函數建議用「test_功能_情境_預期」來命名,比如:test_add_negative_numbers_returns_negative_sum,未來 debug 或報錯時一看就知道測什麼。 -
遇到 bug 先寫測試
修 bug 的時候,可以先寫一個會失敗的測試(重現這個 bug),然後再修 code,最後驗證 bug 修好了,這樣可以防止未來又踩到同樣的坑。 -
不要只測「對」的情況,也要測「錯」的情況
除了基本功能(happy path),還要測試異常輸入、邊界值(edge case)、例外處理,像是 add(None, 3) 這種 case,讓你的程式更 robust。 -
自動化執行,養成 CI 習慣
把測試流程自動化,每次 push code 或合併 PR 就自動跑測試,避免「本地好好的,上線就炸鍋」的慘劇。現在 GitHub Actions 或 GitLab CI 設定都很方便。 -
盡量避免測「外部資源」
測試時最好不要連到真的資料庫、API 或檔案系統,這會讓測試變慢也更容易失敗。遇到這種情況建議用 mock、fake 或 fixture 來模擬行為。 -
失敗時要看得懂訊息
assert
語句可以加上錯誤訊息,例如assert result == 5, "add(2,3) 應該等於 5"
,失敗時馬上知道哪裡怪怪的。 -
寫測試就是寫文件
好的單元測試其實也是一種「活文件」(living documentation),以後自己或別人接手專案,可以從測試案例快速理解每個 function 的用途和預期行為。
四. 結語 #
總之,不管你是用內建的 unittest
,還是第三方的 pytest
,重點就是一定要養成寫單元測試的好習慣。程式越大,測試越能救你一命,也能讓你 refactor 時心情平靜、頭髮不掉!
下次遇到「昨天能跑今天就壞掉」的時候,不要再抓頭髮了,讓單元測試來保護你吧!