練習
在這個練習中,你會使用 pytest with @pytest.mark.parametrize 來測試函式,包括預期例外的情況。 接著,您會將類別型測試從 xUnit 樣式的 setup_method() 和 teardown_method() 方法重構為 fixture。 使用參數化和夾具能幫助你撰寫涵蓋更多案例、減少重複且設定更清晰的測試。
如需設定協助,請參閱 virtual environments 的 Python 文件、pip install 的 pip 文件,以及 installing pytest 的文件。 欲了解更多關於本練習中 pytest 功能的細節,請參閱 pytest 文件,內容包括 測試函式參數化、 預期例外的斷言、 夾具、 xUnit 風格設定,以及 暫存目錄與檔案。
開始之前
請依模組先修條件使用Python 3.10或更新版本。 如果你已經有一個安裝並啟用 pytest 的虛擬環境,請繼續步驟 1。 否則,請在你要建立 test_advanced.py的資料夾裡建立並啟用虛擬環境。
在 Windows PowerShell 上:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
在 macOS、Linux 或 Windows 子系統 Linux 版 上:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
version 指令應該會印出一個 pytest 版本,類似以下輸出:
pytest x.y.z
安裝指令不會把 pytest 釘選到特定版本。 Pip 使用套件中繼資料來選擇與虛擬環境的 Python 版本相容的穩定 pytest 版本。
在整個練習過程中使用 python -m pytest ,使 pytest 遠離活躍環境。 如果 Windows PowerShell 阻擋了 Activate.ps1,請參考 Python venv 文件中的 PowerShell 執行政策選項,或直接執行虛擬環境的 Python來執行 pip 和 pytest 指令。 例如:
.\.venv\Scripts\python -m pip install --upgrade pip
.\.venv\Scripts\python -m pip install pytest
.\.venv\Scripts\python -m pytest -v test_advanced.py
步驟 1 - 在此練習中新增一個測試檔案
在執行 pytest 的同一個資料夾裡,建立一個名為 test_advanced.py 的新測試檔。 不要把檔案放進 .venv 資料夾裡。 新增下列程式碼:
import pytest def str_to_bool(string): normalized = string.lower() if normalized in ["yes", "y", "1"]: return True if normalized in ["no", "n", "0"]: return False raise ValueError(f"Cannot convert {string!r} to a boolean")函數
str_to_bool()接受字串作為輸入。 對於已識別的真值,回傳True;對於已識別的假值,回傳False;對其他值則引發ValueError。請在相同檔案中附加
str_to_bool()函式的測試。 先用@pytest.mark.parametrize來測試真實數值:@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"]) def test_str_to_bool_true(string): assert str_to_bool(string) is True接下來,附加另一個包含「false」值的測試:
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"]) def test_str_to_bool_false(string): assert str_to_bool(string) is False現在有兩個測試函式,涵蓋
True和False傳回值的代表性輸入。最後,附加一個參數化測試,找出函數不應該接受的值。 將
pytest.raises()用作上下文管理器,使測試僅在str_to_bool()引發預期例外時通過。@pytest.mark.parametrize("string", ["maybe", "2"]) def test_str_to_bool_invalid(string): with pytest.raises(ValueError, match="Cannot convert"): str_to_bool(string)參數
match會用正則表達式檢查異常訊息。 在這裡,測試只檢查訊息的穩定部分,因此不依賴於精確的無效值。
注意
為了簡化起見,這個練習會將被測程式碼和測試放在同一個檔案裡。 在實際的 Python 專案中,應用程式程式碼與測試通常會被拆分成不同的檔案和目錄,例如 src/ 套件與 tests/ 目錄。
步驟 2 - 執行測試和探索報告
加入測試後,執行 pytest 並檢查輸出。 使用提高詳細程度的旗標 (-v),讓您可以看到每個輸入值都被視為個別測試。
在 Windows PowerShell 上:
python -m pytest -v test_advanced.py
在 macOS、Linux 或 Windows 子系統 Linux 版 上:
python -m pytest -v test_advanced.py
結果應與以下報告相似:
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 10 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 10%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 20%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 30%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 40%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 50%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 60%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 70%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 80%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 90%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [100%]
============================== 10 passed in 0.01s ==============================
你的作業系統、Python 版本、pytest 版本、執行檔路徑、根目錄、插件和快取行、集合進度格式、行換行和執行時間可能不同。 雖然你只寫了三個測試函數,但 pytest 收集了 10 個測試,因為每個參數化輸入值都當作獨立的測試案例執行。
步驟 3 - 將 setup 和 teardown 重構為 fixture
在 test_advanced.py 檔案中附加一個基於類別的測試。 此測試使用 xUnit 風格
setup_method()及teardown_method()方法,在每個測試方法周圍建立與移除檔案:from pathlib import Path class TestFile: def setup_method(self): self.path = Path("done") self.path.write_text("1", encoding="utf-8") def teardown_method(self): self.path.unlink(missing_ok=True) def test_done_file(self): contents = self.path.read_text(encoding="utf-8") assert contents == "1"此測試可通過,但在目前工作目錄中寫入固定檔名,且需獨立清理程式碼。 若測試執行中斷或同時執行,例如使用
pytest-xdist或重疊的本地執行,此方法可能會留下過時的檔案、覆蓋已存在的名為 done 的檔案,或導致測試間相互影響。新增一個模組層級的夾具,使用內建
tmp_path夾具來建立暫存檔案。 在下一個步驟中,您會更新類別,以使用此 fixture 取代 setup 和 teardown 方法。 為了提高可讀性,請將此模組層級 fixture 放在已參數化的str_to_bool測試之後,並放在TestFile類別之前。 無論模組層級 fixture 出現在模組中的哪個位置,Pytest 都會探索到,因此這個位置是為了方便閱讀,而不是為了 pytest:@pytest.fixture def tmp_file(tmp_path): def write(): path = tmp_path / "done" path.write_text("1", encoding="utf-8") return path return write該
tmp_file()夾具使用 pytest 的tmp_path夾具,為每個測試函式調用(包括每個參數化案例)提供獨特的pathlib.Path暫存目錄。 Pytest 管理的是暫存目錄,因此測試不依賴硬編碼的檔案路徑。刪除先前的
TestFile類別,包括其中的setup_method()和teardown_method()方法,並以下列使用 fixture 取代 setup 和 teardown 方法的最終版本替換:class TestFile: def test_done_file(self, tmp_file): path = tmp_file() contents = path.read_text(encoding="utf-8") assert contents == "1"你也可以移除在前一步中新增的
from pathlib import Path匯入,因為測試不再直接建立Path物件。
檢查您的工作
到現在你應該有一個名為 test_advanced.py 的 Python 檔案,裡面包含:
- 接受字串並針對已辨識的 true 和 false 值傳回布林值的
str_to_bool()函式。 - 對
str_to_bool()函數進行三個參數化測試:一個測試True值,一個測試False值,一個檢查無效值是否引發ValueError。 - 自訂 pytest fixture,使用內建的
tmp_path建立包含一些內容的暫存 done 檔案。 - 一個
TestFile類別,只有一個方法,使用自訂tmp_file夾具來建立和讀取檔案。 - 最終版本沒有
from pathlib import Path匯入,也沒有setup_method()/teardown_method()方法。
最終檔案應該看起來像以下這樣:
import pytest
def str_to_bool(string):
normalized = string.lower()
if normalized in ["yes", "y", "1"]:
return True
if normalized in ["no", "n", "0"]:
return False
raise ValueError(f"Cannot convert {string!r} to a boolean")
@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"])
def test_str_to_bool_true(string):
assert str_to_bool(string) is True
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"])
def test_str_to_bool_false(string):
assert str_to_bool(string) is False
@pytest.mark.parametrize("string", ["maybe", "2"])
def test_str_to_bool_invalid(string):
with pytest.raises(ValueError, match="Cannot convert"):
str_to_bool(string)
@pytest.fixture
def tmp_file(tmp_path):
def write():
path = tmp_path / "done"
path.write_text("1", encoding="utf-8")
return path
return write
class TestFile:
def test_done_file(self, tmp_file):
path = tmp_file()
contents = path.read_text(encoding="utf-8")
assert contents == "1"
再跑一次測試檔。
在 Windows PowerShell 上:
python -m pytest -v test_advanced.py
在 macOS、Linux 或 Windows 子系統 Linux 版 上:
python -m pytest -v test_advanced.py
最終報告應顯示收集的11項檢測:10例參數化檢測及1項類別檢測。
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 11 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 9%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 18%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 27%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 36%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 45%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 54%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 63%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 72%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 81%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [ 90%]
test_advanced.py::TestFile::test_done_file PASSED [100%]
============================== 11 passed in 0.01s ==============================
所有測試都應該無錯誤地通過。