快轉到主要內容
  1. 教學文章/

開發的好習慣 Unit Test

·5 分鐘· loading · loading · ·
Python Pytest Ci Unittest
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 11: 本文

一. 什麼是 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

三. 小技巧
#

  1. 功能要小,測試才有效!
    每個單元測試都應該只針對「一個」功能點或情境來驗證,測太多事情反而不容易查錯。簡單來說,就是一個測試只檢查一種結果,出錯時才容易定位。

  2. 命名要清楚,不怕長,只怕看不懂
    測試函數建議用「test_功能_情境_預期」來命名,比如:test_add_negative_numbers_returns_negative_sum,未來 debug 或報錯時一看就知道測什麼。

  3. 遇到 bug 先寫測試
    修 bug 的時候,可以先寫一個會失敗的測試(重現這個 bug),然後再修 code,最後驗證 bug 修好了,這樣可以防止未來又踩到同樣的坑。

  4. 不要只測「對」的情況,也要測「錯」的情況
    除了基本功能(happy path),還要測試異常輸入、邊界值(edge case)、例外處理,像是 add(None, 3) 這種 case,讓你的程式更 robust。

  5. 自動化執行,養成 CI 習慣
    把測試流程自動化,每次 push code 或合併 PR 就自動跑測試,避免「本地好好的,上線就炸鍋」的慘劇。現在 GitHub Actions 或 GitLab CI 設定都很方便。

  6. 盡量避免測「外部資源」
    測試時最好不要連到真的資料庫、API 或檔案系統,這會讓測試變慢也更容易失敗。遇到這種情況建議用 mock、fake 或 fixture 來模擬行為。

  7. 失敗時要看得懂訊息
    assert 語句可以加上錯誤訊息,例如 assert result == 5, "add(2,3) 應該等於 5",失敗時馬上知道哪裡怪怪的。

  8. 寫測試就是寫文件
    好的單元測試其實也是一種「活文件」(living documentation),以後自己或別人接手專案,可以從測試案例快速理解每個 function 的用途和預期行為。

四. 結語
#

總之,不管你是用內建的 unittest,還是第三方的 pytest,重點就是一定要養成寫單元測試的好習慣。程式越大,測試越能救你一命,也能讓你 refactor 時心情平靜、頭髮不掉!

下次遇到「昨天能跑今天就壞掉」的時候,不要再抓頭髮了,讓單元測試來保護你吧!

Python 學習 - 本文屬於一個選集。
§ 11: 本文

相關文章

管理秘密環境變數 python-dotenv
·1 分鐘· loading · loading
Python Dotenv
在 iPhone 上也可以使用本地 LLM!
·2 分鐘· loading · loading
好玩軟體 LLM Ollama
iPadOS 26 搶先體驗!
·3 分鐘· loading · loading
搶先體驗 Ipad Os
用 NAS 搭設遠端 Linux: Webtop 教學
·3 分鐘· loading · loading
好玩軟體 Linux Webtop
設置和管理 SSH 金鑰
·2 分鐘· loading · loading
SSH
超好用的本地LLM: Ollama
·6 分鐘· loading · loading
好玩軟體 LLM Ollama