介紹 pytest:Python 測試框架的強大選擇
更新日期: 2025 年 2 月 13 日
本文為 AI 描述優化 api 設計 系列文,第 8 篇:
- 如何設計一個商品描述優化 API?—— 完整指南
- 設計 AI 優化商品描述的 Prompt
- FastAPI:高效且易用的 Python Web 框架
- 介紹 Uvicorn:高效能 ASGI 伺服器
- Uvicorn 監聽 0.0.0.0,但為何 API 只能用 127.0.0.1 訪問?
- Tenacity:強大的 Python 重試機制庫
- FastAPI 建立商品描述優化 API
- 介紹 pytest:Python 測試框架的強大選擇 👈進度
- Python httpx 完整指南:高效的 HTTP 客戶端
- Pytest-Benchmark:高效能測試的基準分析工具
- 深入解析 monkeypatch:Python 測試中的強大工具
- API 自動化測試與效能優化
建議閱讀本文前,先具備 聊天機器人-建議回復 相關系列文的概念。
在現代軟體開發中,測試是確保程式正確性、穩定性與可維護性的關鍵步驟。
Python 作為一個廣泛使用的語言,擁有多種測試框架,其中 pytest 是最受歡迎的一款。
pytest
以其簡潔的語法、強大的功能及靈活的擴展性,使開發者能夠更輕鬆地編寫與執行測試。
本篇文章將詳細介紹 pytest
,包含其 安裝方式、基本用法、進階功能、API 測試方法,讓你能夠快速上手並提升測試效率。
什麼是 pytest?
pytest
是 Python 生態系統中最流行的測試框架之一,主要用來執行 單元測試(Unit Test) 和 功能測試(Functional Test)。
相較於 Python 內建的 unittest
,pytest
提供了更簡單的語法、強大的測試工具,以及更好的擴展性,讓測試變得更加直覺且高效。
pytest 的主要特色
- 簡潔直覺的語法:可以用函式來定義測試,而無需繼承測試類別。
- 自動測試發現:
pytest
會自動尋找符合規範的測試函式,無需手動註冊。 - 強大的 fixture 機制:可以輕鬆設定與清理測試環境。
- 內建參數化測試:能夠輕鬆測試多組輸入輸出組合,減少重複程式碼。
- 詳細的錯誤報告:當測試失敗時,
pytest
會提供清楚的錯誤訊息與變數值,方便除錯。 - 豐富的外掛系統:支援許多擴充套件,如
pytest-django
、pytest-cov
(測試覆蓋率分析)等。
安裝 pytest
pytest
可以透過 pip
安裝,執行以下指令:
pip install pytest
安裝完成後,透過以下指令確認 pytest
是否安裝成功:
pytest --version
如果成功安裝,會顯示 pytest
的版本資訊。
基本用法
撰寫第一個測試
pytest
允許我們用簡單的函式來定義測試,不需要建立類別。以下是一個基本測試範例:
# test_sample.py
def test_addition():
assert 1 + 1 == 2
def test_subtraction():
assert 5 - 3 == 2
然後,在終端機執行 pytest
:
pytest
pytest
會自動尋找所有 test_
開頭的函式並執行測試,並顯示測試結果:
============================= test session starts =============================
collected 2 items
test_sample.py .. [100%]
============================== 2 passed in 0.12s ==============================
「..
」 表示兩個測試通過,整體測試成功。
assert
是什麼?
assert
是 Python 的內建關鍵字,用來進行「斷言(Assertion)」,通常用於測試或除錯,確保某個條件為 True,否則會拋出 AssertionError
並終止程式。
在 pytest
測試中,assert
是最常見的語法,因為 pytest
會自動擷取 AssertionError
,並顯示詳細的錯誤訊息,幫助開發者快速定位問題。
assert
的基本用法
語法:
assert 條件表達式, "錯誤訊息(可選)"
如果 條件表達式
為 True
,程式會繼續執行;如果為 False
,則會拋出 AssertionError
,並可選擇性顯示錯誤訊息。
範例 1:簡單的 assert
assert 1 + 1 == 2 # 正確,不會報錯
assert 2 * 2 == 5 # 這行會拋出 AssertionError
執行後:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AssertionError
範例 2:帶錯誤訊息的 assert
assert 10 > 5, "10 應該要大於 5" # 正確,不會報錯
assert 3 > 5, "3 不可能大於 5" # 這行會拋出 AssertionError,並顯示錯誤訊息
執行後:
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AssertionError: 3 不可能大於 5
assert
在 pytest
測試中的應用
在 pytest
測試中,assert
主要用來檢查函式回傳值是否符合預期。
範例 3:基本測試
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5 # 測試通過
assert add(1, -1) == 0 # 測試通過
執行 pytest
:
test_sample.py .. [100%] # 代表 2 個測試通過
assert
與 if
的差異
比較項目 | assert | if 判斷 |
---|---|---|
用途 | 驗證條件是否成立,若 False 則拋出錯誤 | 用於流程控制,不會自動拋錯 |
是否影響程式執行 | 會終止程式並拋出 AssertionError | 不會終止程式,除非手動拋錯 |
使用時機 | 通常用於測試或除錯 | 用於一般程式邏輯 |
範例 4:assert
vs if
x = 10
# 使用 assert
assert x > 5, "x 應該要大於 5" # 如果 x <= 5,程式會拋出錯誤並終止
# 使用 if
if x <= 5:
print("錯誤:x 應該要大於 5") # 只是輸出訊息,程式仍然繼續執行
進階功能
使用 Fixture 設定測試環境
什麼是 Fixture?
在測試中,我們經常需要 準備測試數據 或 初始化一些資源,例如:
- 建立 測試資料(像是模擬的使用者、商品清單等)。
- 建立 資料庫連線 或 打開檔案。
- 設定 API 的模擬回應(Mock API)。
- 在測試結束後進行清理(像是關閉資料庫連線、刪除測試檔案等)。
在 pytest
中,Fixture 是一種特殊的函式,它可以在測試前執行 初始化,並在測試後執行 清理工作,確保測試環境乾淨且穩定。
Fixture 的基本用法
假設我們要測試一個函式 sum()
,但每次測試都需要準備一組相同的數據 [1, 2, 3, 4, 5]
。
如果不使用 Fixture,我們的測試可能會這樣寫:
def test_sum():
sample_data = [1, 2, 3, 4, 5] # 手動建立測試數據
assert sum(sample_data) == 15
這種寫法沒問題,但如果 多個測試都需要相同的數據,我們就需要 重複寫很多次,這樣不夠優雅。
使用 Fixture 來優化測試
import pytest
# 定義一個 fixture,準備測試數據
@pytest.fixture
def sample_data():
return [1, 2, 3, 4, 5]
# 測試函式會自動使用 sample_data
def test_sum(sample_data):
assert sum(sample_data) == 15
執行步驟解析:
pytest
會發現@pytest.fixture
裝飾的sample_data()
,並視為 測試用的前置準備。- 當
pytest
執行test_sum(sample_data)
時,它會自動 執行sample_data()
,並把結果傳給test_sum()
。 test_sum()
接收sample_data
,然後執行assert sum(sample_data) == 15
,檢查結果是否正確。
這樣的好處是:
✅ 可重複使用:其他測試函式也可以使用 sample_data
,不用每次都手動建立數據。
✅ 可擴展:我們可以進一步修改 Fixture,例如在測試結束後執行清理工作(像是關閉資料庫連線)。
Fixture 的進階用法
如果我們需要在測試開始前準備資料,測試結束後清理資料,可以這樣做:
import pytest
@pytest.fixture
def setup_and_cleanup():
print("=== 測試開始前:初始化 ===")
data = {"name": "Alice", "age": 25} # 準備測試數據
yield data # yield 之前的程式碼在測試前執行
print("=== 測試結束後:清理 ===") # yield 之後的程式碼在測試後執行
def test_user_data(setup_and_cleanup):
user = setup_and_cleanup # 取得 fixture 回傳的測試數據
assert user["name"] == "Alice"
assert user["age"] == 25
執行結果:
=== 測試開始前:初始化 ===
.
=== 測試結束後:清理 ===
yield
之前的部分 在測試開始前執行,yield
之後的部分 在測試結束後執行,這樣我們可以在測試後執行清理工作。
參數化測試(Parameterized Testing)
什麼是參數化測試?
有時候,我們需要針對「不同輸入數據」測試相同的函式。例如,我們希望測試 add(a, b)
的行為:
def add(a, b):
return a + b
我們可能會這樣測試:
def test_add():
assert add(1, 2) == 3
assert add(5, 5) == 10
assert add(10, -1) == 9
雖然這樣也可以,但這些 assert
都寫在同一個函式內,當其中一個測試失敗時,後面的測試不會執行,而且這樣的寫法也不夠靈活。
使用 @pytest.mark.parametrize
來簡化測試
pytest
提供 參數化測試(Parameterized Testing),讓我們可以用 一個測試函式 測試 多組輸入數據:
import pytest
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3), # 測試 add(1, 2) 是否等於 3
(5, 5, 10), # 測試 add(5, 5) 是否等於 10
(10, -1, 9) # 測試 add(10, -1) 是否等於 9
])
def test_add(a, b, expected):
assert add(a, b) == expected
執行步驟解析:
pytest.mark.parametrize("a, b, expected", [...])
定義了 多組測試數據,每組數據有 三個參數(a, b, expected)。pytest
會針對 每組數據 執行test_add()
,傳入對應的a, b, expected
。- 測試時,
assert add(a, b) == expected
會對比函式輸出與期望值,確保正確性。
測試輸出結果
test_sample.py ... [100%]
...
表示 3 個測試全部通過。
如果某個測試失敗呢?
假設 add(10, -1)
回傳了 8
(應該是 9
),pytest
會清楚顯示錯誤:
E assert 8 == 9
E + where 8 = add(10, -1)
這樣我們能 快速定位問題,不需要手動檢查多個 assert
。
測試 API(搭配 httpx
)
除了單元測試,pytest
也能用來測試 API,這對於開發 RESTful API 的專案非常有幫助。
我們可以使用 httpx
來發送 HTTP 請求,然後檢查 API 是否回傳正確的結果。
安裝 httpx
pip install httpx
API 測試範例
假設我們有一個 API,提供 /users/1
端點,回傳特定使用者的資料:
import httpx
def test_get_user():
response = httpx.get("https://jsonplaceholder.typicode.com/users/1")
assert response.status_code == 200
assert response.json()["id"] == 1
assert response.json()["name"] == "Leanne Graham"
這段測試程式碼會發送 GET 請求到 https://jsonplaceholder.typicode.com/users/1
,然後檢查:
- HTTP 狀態碼是否為
200
(請求成功)。 - 回傳的 JSON 資料是否包含
id=1
和name="Leanne Graham"
。
模擬 API 回應?
在測試 API 時,我們通常會向某個伺服器發送 HTTP 請求,但在某些情況下,我們 不希望真的發送請求,例如:
- 伺服器還沒開發完成:前端或其他模組需要測試,但 API 還未實作。
- 避免影響正式環境:測試時如果真的發送請求,可能會對正式資料產生影響(例如新增資料、刪除用戶等)。
- 提高測試速度:如果每次測試都要連線到遠端伺服器,會導致測試變慢。
- 測試錯誤處理機制:我們可以模擬伺服器回應錯誤(例如
500 Internal Server Error
),看看程式是否能正確處理異常。
為了解決這些問題,我們可以 「模擬 API 回應」,讓 httpx.get()
或 httpx.post()
不會真的發送 HTTP 請求,而是直接回傳一個 假裝來自伺服器的回應。
使用 pytest-httpx
來模擬 API 回應
pytest-httpx
是一個 pytest
的外掛,可以幫助我們 攔截 HTTP 請求並回傳模擬的回應,這樣測試時就不會真的發送請求。
步驟 1:安裝 pytest-httpx
首先,你需要安裝 pytest-httpx
:
pip install pytest-httpx
這個外掛可以幫助我們模擬 HTTP 回應,而不是真的發送請求。
範例:模擬 API 回應
假設我們有一個 API,提供 /data
端點,會回傳以下 JSON:
{
"message": "success"
}
我們的程式會透過 httpx.get("https://api.example.com/data")
來取得這個資料。
但是,我們不想真的發送請求,而是要模擬這個 API 回應,這時可以用 pytest-httpx
來攔截請求並回傳假數據。
步驟 2:建立測試程式
import httpx
import pytest
@pytest.fixture
def mock_httpx_response(httpx_mock):
# 使用 httpx_mock 來攔截請求,並模擬回應
httpx_mock.add_response(
url="https://api.example.com/data", # 指定要攔截的 API URL
json={"message": "success"}, # 模擬 API 回傳的 JSON 資料
status_code=200 # 設定 HTTP 狀態碼為 200(成功)
)
def test_api(mock_httpx_response):
# 這裡的 httpx.get() 不會真的發送 HTTP 請求,而是直接回傳模擬的回應
response = httpx.get("https://api.example.com/data")
# 驗證回應的狀態碼是否正確
assert response.status_code == 200
# 驗證回應的 JSON 內容是否正確
assert response.json() == {"message": "success"}
這段程式碼在做什麼?
1. httpx_mock.add_response()
攔截請求並回應模擬數據
httpx_mock.add_response()
會 攔截所有發送到https://api.example.com/data
的請求,並回傳設定好的模擬數據。json={"message": "success"}
指定 回傳的 JSON 內容。status_code=200
指定 回傳的 HTTP 狀態碼。
2. test_api()
測試函式
- 當
httpx.get("https://api.example.com/data")
被執行時,httpx_mock
會 攔截這個請求,不會真的發送到網路上,而是直接回傳{"message": "success"}
。 assert response.status_code == 200
確保狀態碼正確。assert response.json() == {"message": "success"}
確保回應內容正確。
範例:模擬不同的 API 回應
有時候,我們想測試 API 發生錯誤的情況(例如 404 Not Found 或 500 伺服器錯誤),這時也可以透過 httpx_mock.add_response()
來模擬不同的錯誤回應。
1. 模擬 404 Not Found
@pytest.fixture
def mock_404_response(httpx_mock):
httpx_mock.add_response(
url="https://api.example.com/notfound",
status_code=404,
json={"error": "Not Found"}
)
def test_api_404(mock_404_response):
response = httpx.get("https://api.example.com/notfound")
assert response.status_code == 404
assert response.json() == {"error": "Not Found"}
這樣 httpx.get("https://api.example.com/notfound")
不會真的發送請求,而是直接回傳 404
錯誤。
2. 模擬 500 Internal Server Error
@pytest.fixture
def mock_500_response(httpx_mock):
httpx_mock.add_response(
url="https://api.example.com/server-error",
status_code=500,
json={"error": "Internal Server Error"}
)
def test_api_500(mock_500_response):
response = httpx.get("https://api.example.com/server-error")
assert response.status_code == 500
assert response.json() == {"error": "Internal Server Error"}
這樣我們可以測試當 API 發生 500 錯誤時,我們的程式是否能正確處理。
這樣做有什麼好處?
- 測試不會影響真實伺服器
- 我們 不會真的發送請求,這樣測試不會影響正式環境(例如不會誤刪資料)。
- 測試速度更快
- 如果 API 需要 1 秒 才回應,測試時可能會拖慢速度。但用
pytest-httpx
,回應是模擬的,測試可以在 毫秒內完成。
- 如果 API 需要 1 秒 才回應,測試時可能會拖慢速度。但用
- 可以測試 API 異常情境
- 你可以 模擬 404、500 或其他錯誤,確保你的程式能正確處理 API 失敗的情況。
結論
pytest
是一款功能強大且靈活的 Python 測試框架,適用於 單元測試、功能測試及 API 測試。
透過其 簡潔的語法、自動測試發現、fixture 機制、參數化測試。
pytest
大幅提升測試的易用性與可維護性,讓開發者能更輕鬆地確保程式的品質與穩定性。
此外,搭配 httpx
或 pytest-httpx
,pytest
也能用來測試 API,確保你的後端服務能夠正常運作。
希望這篇文章能幫助你更好地理解 pytest
,讓你的測試工作更加順利! 🚀