練習
在此練習中,您會使用 Pytest 來測試函式。 接著,您便可發現並修正造成函式測試失敗的一些潛在問題。 查看失敗情況並使用 Pytest 的進階錯誤報告,對識別並修正生產程式碼中有問題的測試或錯誤來說非常重要。
在此練習中,我們會使用名為「admin_command()
」的函式,您可以在該函式中輸入系統命令,並選用 sudo
工具作為首碼。 您會透過撰寫測試來尋找函式中的錯誤。
步驟 1 - 在此練習中新增一個測試檔案
使用 Python 的測試檔案檔案名慣例,建立新的測試檔案。 將測試檔案命名為 test_exercise.py 然後新增至以下代碼:
def admin_command(command, sudo=True): """ Prefix a command with `sudo` unless it is explicitly not needed. Expects `command` to be a list. """ if sudo: ["sudo"] + command return command
您可以使用
command
引數在函式admin_command()
中輸入清單,並選用sudo
作為清單首碼。 如果將sudo
關鍵字引數設為False
,系統就會傳回與輸入相同的命令。請在相同檔案中附加
admin_command()
函式的測試。 測試將採用協助程式法來傳回樣本命令:class TestAdminCommand: def command(self): return ["ps", "aux"] def test_no_sudo(self): result = admin_command(self.command(), sudo=False) assert result == self.command() def test_sudo(self): result = admin_command(self.command(), sudo=True) expected = ["sudo"] + self.command() assert result == expected
注意
在與實際程式碼相同的檔案中執行測試並不常見。 為了操作簡單,本練習中的範例會在相同檔案內執行實際程式碼。 在實際的 Python 專案中,測試通常是以測試中程式碼的檔案和目錄作區隔。
步驟 2 - 執行測試並識別失敗
既然測試檔案已有一個可測試的函式,以及幾種能驗證其行為的測試,那就可以開始執行測試並處理失敗情況了。
使用 Python 執行檔案:
$ pytest test_exercise.py
執行應該會完成一個測試通過和一個失敗,而失敗輸出應該類似下列輸出:
=================================== FAILURES =================================== __________________________ TestAdminCommand.test_sudo __________________________ self = <test_exercise.TestAdminCommand object at 0x10634c2e0> def test_sudo(self): result = admin_command(self.command(), sudo=True) expected = ["sudo"] + self.command() > assert result == expected E AssertionError: assert ['ps', 'aux'] == ['sudo', 'ps', 'aux'] E At index 0 diff: 'ps' != 'sudo' E Right contains one more item: 'aux' E Use -v to get the full diff test_exercise.py:24: AssertionError =========================== short test summary info ============================ FAILED test_exercise.py::TestAdminCommand::test_sudo - AssertionError: assert... ========================= 1 failed, 1 passed in 0.04s ==========================
test_sudo()
測試上的輸出失敗。 Pytest 正在提供兩個比較清單的詳細資訊。 在此情況下,result
變數不包含sudo
命令,這是預期中的測試結果。
步驟 3 - 修正 Bug 並讓測試通過
進行任何變更之前,您必須先瞭解一開始為什麼會失敗。 雖然您可以看出不符合預期 (結果中不包含 sudo
),但您必須了解原因。
符合 sudo=True
條件時,請查看 admin_command()
函式中下列幾行程式碼:
if sudo:
["sudo"] + command
不會使用清單作業來傳回數值。 由於不會傳回,因此函式最後便會一律在沒有 sudo
的情況下傳回命令。
更新
admin_command()
函式即可傳回清單作業,以便在要求sudo
命令時使用修改後的結果。 更新的函式如下所示:def admin_command(command, sudo=True): """ Prefix a command with `sudo` unless it is explicitly not needed. Expects `command` to be a list. """ if sudo: return ["sudo"] + command return command
使用 Pytest 重新執行測試。 請試著透過 Pytest 使用
-v
旗標來增加輸出的詳細程度:$ pytest -v test_exercise.py
現在請確認輸出。 它現在應該會顯示兩個通過的測試:
============================= test session starts ============================== Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 cachedir: .pytest_cache rootdir: /private collected 2 items test_exercise.py::TestAdminCommand::test_no_sudo PASSED [ 50%] test_exercise.py::TestAdminCommand::test_sudo PASSED [100%] ============================== 2 passed in 0.00s ===============================
注意
由於函式能夠處理不同大小寫的更多值,因此應該新增更多測試來涵蓋這些變化。 這可防止將來對函式的變更導致不同的 (非預期) 行為。
步驟 4 - 使用測試新增程式碼
在先前步驟中新增測試之後,您應該會有自信對函式進行更多變更,並使用測試來驗證。 即便現有測試未涵蓋這些變更,您仍不會中斷任何先前的假設。
在此情況下,admin_command()
函式會一律認為 command
引數是清單。 我們可以引發具實用錯誤訊息的例外狀況來加以改善。
首先,建立可擷取行為的測試。 雖然函式尚未更新,但您可以嘗試以測試為主的方法 (也稱為測試驅動開發或 TDD)。
- 更新 test_exercise.py 檔案,方便其在頂部匯入
pytest
。 此測試會使用pytest
架構的內部協助程式:
import pytest
- 現在,將新測試附加至類別中以檢查例外狀況。 此測試應預期當傳至函式的值不是清單時,函式會回傳
TypeError
:
def test_non_list_commands(self): with pytest.raises(TypeError): admin_command("some command", sudo=True)
- 更新 test_exercise.py 檔案,方便其在頂部匯入
使用 Pytest 再次執行測試後應該就能全部通過:
============================= test session starts ============================== Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 rootdir: /private/ collected 3 items test_exercise.py ... [100%] ============================== 3 passed in 0.00s ===============================
該測試足以檢查
TypeError
,但最好新增具實用錯誤訊息的程式碼。更新函式即可透過有用的錯誤訊息明確引發
TypeError
:def admin_command(command, sudo=True): """ Prefix a command with `sudo` unless it is explicitly not needed. Expects `command` to be a list. """ if not isinstance(command, list): raise TypeError(f"was expecting command to be a list, but got a {type(command)}") if sudo: return ["sudo"] + command return command
最後,更新
test_non_list_commands()
方法即可檢查錯誤訊息:def test_non_list_commands(self): with pytest.raises(TypeError) as error: admin_command("some command", sudo=True) assert error.value.args[0] == "was expecting command to be a list, but got a <class 'str'>"
更新的測試會使用
error
作為保存所有例外狀況資訊的變數。 您可以使用error.value.args
來查看例外狀況的引數。 在此情況下,第一個引數便含有可透過測試檢查的錯誤字串。
檢查您的工作
此時,您應該就會得到一個名為「test_exercise.py」的 Python 測試檔案,且包含:
- 接受引數和關鍵字引數的
admin_command()
函式。 admin_command()
函式中具有實用錯誤訊息的TypeError
例外狀況。- 具
command()
協助程式方法和三種測試方法的TestAdminCommand()
測試類別,可檢查admin_command()
函式。
在終端執行測試時,所有測試都應通過且無錯誤。