Python 測試好幫手 pytest 模組入門

本文帶大家入門 pytest 模組,開始為自己的 Python 程式碼撰寫測試。測試 Code 不僅可以確保既有功能,也經常可以提前捕捉許多潛在 Bug。一種常用測試手法稱為 Unittest,旨在為每個最小單元功能做全方面測試,以保證功能運作符合預期。

Python 內建就有一個 Unittest 模組可用,不過由於語法繁瑣,強烈推薦大家使用本文主角:pytest

安裝

$ pip install pytest

測試目標

假設 main.py 中寫了一支 Function 以取得指定 JSON 檔案中的設定值。

import json

def get_config(file: str, key: str, default=None):
    with open(file, "r") as fs:
        settings = json.loads(fs)
        return settings.get(key, default)

撰寫第一支簡單測試

在同目錄新增一個名為 test_main.py 的 Python 檔案。

檔名任意。這裡加入前綴 test_ ,是為了讓 pytest 自動辨識為測試對象。
import main

def test_config():
    a = main.get_config("data.json", "foo", 0)
    assert a == 1   # pytest use built-in assert keyword to validate result

這裡有幾個重點:

  • 將想要執行的測試 Function 名稱加上 test_ 前綴。這樣 pytest 就能認得它。
  • 使用 Python assert 語法檢查程式運行結果。

跑測試

到終端機執行:

$ pytest

如果沒意外,我們會得到第一個 Fail!原因是測試檔案 data.json 不存在。

collected 1 item                                                              

test_main.py F                                                          [100%]

================================== FAILURES ===================================
_________________________________ test_config _________________________________

    def test_config():
>       a = main.get_config("data.json", "foo", 0)

test_main.py:4: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

file = 'data.json', key = 'foo', default = 0

    def get_config(file: str, key: str, default=None):
>       with open(file, "r") as fs:
E       FileNotFoundError: [Errno 2] No such file or directory: 'data.json'

main.py:8: FileNotFoundError
=========================== short test summary info ===========================
FAILED test_main.py::test_config - FileNotFoundError: [Errno 2] No such file or directory: 'data.json'
============================== 1 failed in 0.04s ==============================

預備與收尾

當然,我們可以手動新增測試檔案 data.json,但每次都要這樣做十分麻煩。又如果測試完成想要刪掉它,就可以用 pytest 提供的預備與收尾 Function 來解決這兩個問題!

跑每一支 Function 時做

  • setup_function:執行每一個測試 Function 前執行
  • teardown_function:執行完每一個測試 Function 後執行

我們在 test_main.py 中添加以下內容:

import os
import json

def setup_function():
    with open("data.json", "w") as fs:
        json.dump({"foo": 1}, fs)

def teardown_function():
    os.remove("data.json")

再跑一次測試,成功!

collected 1 item                                                              

test_main.py .                                                          [100%]

============================== 1 passed in 0.00s ==============================

跑每一個檔案時做

  • setup_module:執行單一測試 Python 檔案中,第一個測試 Function 前執行
  • teardown_module:執行完單一測試 Python 檔案中,最後一個測試 Function 後執行

改用上面兩個寫一下:

  • setup_function 改成 setup_module
  • teardown_function改成 teardown_module

再跑一次測試,依然成功!

執行順序說明

test_main.py 檔案中,有另一個測試 Function 假如 test_empty_key

def test_empty_key():
    b = main.get_config("data.json", "bar", -1)
    assert b == -1

執行順序就會如下:

  • setup_module
    • setup_function
      • test_config
    • teardown_function
    • setup_function
      • test_empty_key
    • teardown_function
  • teardown_module

資料夾整理

隨著專案功能逐漸增加,測試檔案也勢必越來越多,全部放在同一層目錄就會顯得臃腫。一種資料夾常見的擺法,就是分別收納至 srctests 資料夾。

./
├── .pyproject.toml   <-- 後面段落說明
├── src/
│   └── main.py
└── tests/
    └── test_config.py

整理完再跑一次測試看看。發現這次 Fail 了!

collected 0 items / 1 error                                                   

=================================== ERRORS ====================================
_____________________ ERROR collecting tests/test_main.py _____________________
ImportError while importing test module '/.../tests/test_main.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/.../3.11/lib/python3.11/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
tests/test_main.py:1: in 
    import main
E   ModuleNotFoundError: No module named 'main'
=========================== short test summary info ===========================
ERROR tests/test_main.py
!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
============================== 1 error in 0.06s ===============================

這是因為檔案移動會造成 pytest 找不到 main.py 檔案。因此我們要加入 pytest 設定,請他去尋找 src 資料夾。

做法是在根目錄新增一特定檔名 pyproject.toml 檔案,內容如下:

[tool.pytest.ini_options]
pythonpath = [
  "src",
]

再跑一次測試,就又成功了!

 

作者挖坑區

若 Function 之間需要共用變數,或者單純想按分類整理測試 Function,也可以選用 Class 方式來撰寫。此外,pytest 還有許多其他強力功能,例如為測試 Function 提供自定義參數(Fixture)、撒測試參數組合(Parametrize)、Mock 掉特定函式等等。

有空會再陸續整理分享。

 

參考文章