pytest 中的类型标注

注意

本页面假定读者熟悉 Python 的类型系统及其优点。

欲了解更多信息,请参阅 Python 类型标注文档

为什么要为测试添加类型标注?

为测试添加类型标注提供了显著优势

  • 可读性: 清晰定义预期的输入和输出,提高了可读性,尤其是在复杂或参数化的测试中。

  • 重构: 这是为测试添加类型标注的主要好处,因为它将极大地帮助重构,让类型检查器指出生产代码和测试中所需的更改,而无需运行完整的测试套件。

对于生产代码,类型标注也有助于捕获一些可能根本无法通过测试(无论覆盖率如何)捕获的 bug,例如:

def get_caption(target: int, items: list[tuple[int, str]]) -> str:
    for value, caption in items:
        if value == target:
            return caption

类型检查器会正确地报错,指出该函数可能返回 None,然而即使是完全覆盖的测试套件也可能错过这种情况。

def test_get_caption() -> None:
    assert get_caption(10, [(1, "foo"), (10, "bar")]) == "bar"

请注意,上述代码的覆盖率是 100%,但并未捕获到 bug(当然,这个例子“很明显”,但旨在说明问题)。

在测试套件中使用类型标注

要在 pytest 中为 fixture 添加类型标注,只需为 fixture 函数添加常规类型即可——仅仅因为使用了 fixture 装饰器,无需做任何特殊的事情。

import pytest


@pytest.fixture
def sample_fixture() -> int:
    return 38

同样地,传递给测试函数的 fixture 需要用 fixture 的返回类型进行标注。

def test_sample_fixture(sample_fixture: int) -> None:
    assert sample_fixture == 38

从类型检查器的角度来看,sample_fixture 实际上是一个由 pytest 管理的 fixture 并不重要,对它来说,重要的是 sample_fixture 是一个 int 类型的参数。

同样的逻辑也适用于 @pytest.mark.parametrize

@pytest.mark.parametrize("input_value, expected_output", [(1, 2), (5, 6), (10, 11)])
def test_increment(input_value: int, expected_output: int) -> None:
    assert input_value + 1 == expected_output

当为接收其他 fixture 的 fixture 函数添加类型标注时,也适用相同的逻辑。

@pytest.fixture
def mock_env_user(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("USER", "TestingUser")

总结

将类型标注纳入 pytest 测试可以增强**清晰度**,改进**调试**和**维护**,并确保**类型安全**。这些实践将带来一个**健壮**、**可读**且**易于维护**的测试套件,能够更好地应对未来的变更,并将错误风险降至最低。