练习
在本练习中,你将结合使用 pytest 和 @pytest.mark.parametrize 来测试一个函数,包括预计会引发异常的用例。 然后,将一个基于类的测试从 xUnit 风格的 setup_method() 和 teardown_method() 方法重构为固定例程。 使用参数化和固定装置有助于编写测试,这些测试涵盖更多案例,且重复程度较低且设置更清晰。
有关设置帮助,请参阅 Python 文档中的虚拟环境部分、pip 文档pip install,以及 pytest 文档中的安装 pytest部分。 有关本练习中 pytest 功能的更多详细信息,请参阅用于 参数化测试函数的 pytest 文档、 有关预期异常、 固定装置、 xUnit 样式设置以及 临时目录和文件的断言。
在您开始之前
按照模块先决条件中所述,使用 Python 3.10 或更高版本。 如果已安装并激活了 pytest 的虚拟环境,请继续执行步骤 1。 否则,请在要在其中创建 test_advanced.py的文件夹中创建和激活虚拟环境。
在 Windows PowerShell 上:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
在 macOS、Linux 或适用于 Linux 的 Windows 子系统上:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
版本命令应打印 pytest 版本,类似于以下输出:
pytest x.y.z
安装命令不会将 pytest 固定到特定版本。 Pip 会根据包元数据选择与虚拟环境中的 Python 版本兼容的稳定 pytest 版本。
在本练习中始终使用 python -m pytest,以确保 pytest 从活动环境中运行。 如果Windows PowerShell 阻止 Activate.ps1,请参阅 PowerShell 执行策略选项的 Python venv 文档,或直接针对 pip 和 pytest 命令运行虚拟环境的Python。 例如:
.\.venv\Scripts\python -m pip install --upgrade pip
.\.venv\Scripts\python -m pip install pytest
.\.venv\Scripts\python -m pytest -v test_advanced.py
步骤 1 - 为本练习添加测试文件
在运行 pytest 的同一文件夹中,创建名为 test_advanced.py的新测试文件。 不要将文件放在 .venv 文件夹中。 添加以下代码:
import pytest def str_to_bool(string): normalized = string.lower() if normalized in ["yes", "y", "1"]: return True if normalized in ["no", "n", "0"]: return False raise ValueError(f"Cannot convert {string!r} to a boolean")该函数
str_to_bool()接受字符串作为输入。 如果识别出的 true 值,它将返回True;如果识别出的 false 值,将返回False;对于任何其他值,将引发ValueError。在同一个文件中,追加对
str_to_bool()函数的测试。 使用@pytest.mark.parametrize首先测试真值:@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"]) def test_str_to_bool_true(string): assert str_to_bool(string) is True接下来,再添加一个使用假值的测试:
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"]) def test_str_to_bool_false(string): assert str_to_bool(string) is False现在有两个测试函数,它们涵盖了
True和False返回值的代表性输入。最后,为函数不应接受的值追加参数化测试。 将
pytest.raises()用作上下文管理器,这样只有在str_to_bool()引发预期异常时,测试才会通过。@pytest.mark.parametrize("string", ["maybe", "2"]) def test_str_to_bool_invalid(string): with pytest.raises(ValueError, match="Cannot convert"): str_to_bool(string)该
match参数使用正则表达式检查异常消息。 在这里,测试仅检查消息的稳定部分,因此它不依赖于确切的无效值。
注意
为简单起见,本练习将测试中的代码和测试保留在同一文件中。 在实际Python项目中,应用程序代码和测试通常分为不同的文件和目录,例如 src/ 包和 tests/ 目录。
步骤 2 - 运行测试并浏览报告
添加测试后,运行 pytest 并检查输出。 使用增加的详细程度标志 (-v),这样便可以看到每个输入值都被视为单独的测试。
在 Windows PowerShell 上:
python -m pytest -v test_advanced.py
在 macOS、Linux 或适用于 Linux 的 Windows 子系统上:
python -m pytest -v test_advanced.py
输出应类似于以下报表:
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 10 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 10%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 20%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 30%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 40%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 50%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 60%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 70%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 80%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 90%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [100%]
============================== 10 passed in 0.01s ==============================
操作系统、Python版本、pytest 版本、可执行路径、根目录、插件和缓存行、集合进度格式、换行和运行时可能有所不同。 虽然只编写了三个测试函数,但 pytest 收集了 10 个测试,因为每个参数化输入值都作为自己的测试用例运行。
步骤 3 - 将 setup 和 teardown 重构为固定例程
将基于类的测试追加到 test_advanced.py 文件中。 此测试使用 xUnit 样式
setup_method()和teardown_method()方法创建和删除每个测试方法周围的文件:from pathlib import Path class TestFile: def setup_method(self): self.path = Path("done") self.path.write_text("1", encoding="utf-8") def teardown_method(self): self.path.unlink(missing_ok=True) def test_done_file(self): contents = self.path.read_text(encoding="utf-8") assert contents == "1"此测试可以通过,但它在当前工作目录中写入固定文件名,并且需要单独的清理代码。 如果测试运行被中断或测试同时进行,例如与
pytest-xdist或本地运行重叠,这种方法可能会留下过时的文件、覆盖一个名为done的现有文件,或导致测试相互影响。添加一个模块级固定例程,它使用内置的
tmp_path固定例程来创建一个临时文件。 在下一步中,你将更新类以使用此固定例程,而不是 setup 和 teardown 方法。 若要提高可读性,请将此模块级固定装置放置在参数化str_to_bool测试之后和类TestFile之前。 Pytest 会自动发现模块级固定例程,无论它们出现在模块中的什么位置,因此该位置仅为方便人类阅读,对 pytest 无实际作用:@pytest.fixture def tmp_file(tmp_path): def write(): path = tmp_path / "done" path.write_text("1", encoding="utf-8") return path return writetmp_file()固定例程使用 pytest 的tmp_path固定例程来为每次测试函数调用(包括每个参数化用例)提供一个唯一的pathlib.Path临时目录。 Pytest 管理临时目录,因此测试不依赖于硬编码的文件路径。删除之前的
TestFile类(包括其setup_method()和teardown_method()方法),并将其替换为使用固定例程而不是 setup 和 teardown 方法的此最终版本:class TestFile: def test_done_file(self, tmp_file): path = tmp_file() contents = path.read_text(encoding="utf-8") assert contents == "1"还可以删除在上一步中添加的
from pathlib import Path导入,因为测试不再直接创建Path对象。
检查你的工作
现在,你应该有一个名为 test_advanced.py 的Python文件,其中包含:
- 一个
str_to_bool()函数,它接受字符串并返回用于识别 true 和 false 值的布尔值。 - 针对函数
str_to_bool()的三个参数化测试:一个用于测试True值,另一个用于测试False值,还有一个用于检查无效值是否会引发ValueError。 - 一个自定义 pytest 固定例程,它使用内置
tmp_path固定例程来创建包含某些内容的临时 done 文件。 - 单个
TestFile类,它包含一个使用自定义tmp_file固定例程来创建和读取文件的方法。 - 在最终版本中没有
from pathlib import Path导入和没有setup_method()/teardown_method()方法。
最终文件应如下所示:
import pytest
def str_to_bool(string):
normalized = string.lower()
if normalized in ["yes", "y", "1"]:
return True
if normalized in ["no", "n", "0"]:
return False
raise ValueError(f"Cannot convert {string!r} to a boolean")
@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"])
def test_str_to_bool_true(string):
assert str_to_bool(string) is True
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"])
def test_str_to_bool_false(string):
assert str_to_bool(string) is False
@pytest.mark.parametrize("string", ["maybe", "2"])
def test_str_to_bool_invalid(string):
with pytest.raises(ValueError, match="Cannot convert"):
str_to_bool(string)
@pytest.fixture
def tmp_file(tmp_path):
def write():
path = tmp_path / "done"
path.write_text("1", encoding="utf-8")
return path
return write
class TestFile:
def test_done_file(self, tmp_file):
path = tmp_file()
contents = path.read_text(encoding="utf-8")
assert contents == "1"
再次运行测试文件。
在 Windows PowerShell 上:
python -m pytest -v test_advanced.py
在 macOS、Linux 或适用于 Linux 的 Windows 子系统上:
python -m pytest -v test_advanced.py
最终报告应显示 11 个收集的测试:参数化测试中的 10 个事例和一个基于类的测试。
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 11 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 9%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 18%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 27%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 36%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 45%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 54%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 63%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 72%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 81%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [ 90%]
test_advanced.py::TestFile::test_done_file PASSED [100%]
============================== 11 passed in 0.01s ==============================
所有测试都应通过,且不会出错。