在 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 装饰器,无需做任何特殊的事情。

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 测试中引入类型提示增强了清晰度,改进了调试维护,并确保了类型安全。这些实践造就了一个健壮可读易于维护的测试套件,能够更好地应对未来的变更,同时将错误风险降至最低。