练习
在本练习中,使用 Pytest 测试函数。 然后发现并修复函数当中导致测试失败的一些潜在问题。 查看失败结果和使用 Pytest 丰富的错误报告功能对于发现和修复生产代码中存在问题的测试或 bug 至关重要。
在本练习中,我们使用称为 admin_command()
的函数,该函数接受系统命令作为输入,并可以使用 sudo
工具为其添加前缀(可选操作)。 该函数有一个 bug,你将通过编写测试来发现该 bug。
步骤 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
,则该参数会返回输入的命令。在同一个文件中,追加对
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()
帮助程序方法和三个检查admin_command()
函数的测试方法的TestAdminCommand()
测试类。
在终端中运行测试时,所有测试均应通过,无任何错误。