练习

已完成

在本练习中,使用 Pytest 测试函数。 然后发现并修复函数当中导致测试失败的一些潜在问题。 查看失败结果和使用 Pytest 丰富的错误报告功能对于发现和修复生产代码中存在问题的测试或 bug 至关重要。

在本练习中,我们使用称为 admin_command() 的函数,该函数接受系统命令作为输入,并可以使用 sudo 工具为其添加前缀(可选操作)。 该函数有一个 bug,你将通过编写测试来发现该 bug。

步骤 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
    

    函数 admin_command() 使用 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() 帮助程序方法和三个检查 admin_command() 函数的测试方法的 TestAdminCommand() 测试类。

在终端中运行测试时,所有测试均应通过,无任何错误。