練習

已完成

在此練習中,您會使用 Pytest 來測試函式。 接著,您便可發現並修正造成函式測試失敗的一些潛在問題。 查看失敗情況並使用 Pytest 的進階錯誤報告,對識別並修正生產程式碼中有問題的測試或錯誤來說非常重要。

在此練習中,我們會使用名為「admin_command()」的函式,您可以在該函式中輸入系統命令,並選用 sudo 工具作為首碼。 您會透過撰寫測試來尋找函式中的錯誤。

步驟 1 - 在此練習中新增一個測試檔案

  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,系統就會傳回與輸入相同的命令。

  2. 請在相同檔案中附加 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 的情況下傳回命令。

  1. 更新 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
    
  2. 使用 Pytest 重新執行測試。 請試著透過 Pytest 使用 -v 旗標來增加輸出的詳細程度:

    $ pytest -v test_exercise.py
    
  3. 現在請確認輸出。 它現在應該會顯示兩個通過的測試:

    ============================= 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 引數是清單。 我們可以引發具實用錯誤訊息的例外狀況來加以改善。

  1. 首先,建立可擷取行為的測試。 雖然函式尚未更新,但您可以嘗試以測試為主的方法 (也稱為測試驅動開發或 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)
    
  2. 使用 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,但最好新增具實用錯誤訊息的程式碼。

  3. 更新函式即可透過有用的錯誤訊息明確引發 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
    
  4. 最後,更新 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() 函式。

在終端執行測試時,所有測試都應通過且無錯誤。