Kodokon kodokon.com

用 pytest 做测试:fixture、参数化、最佳实践

用 pytest 构建稳健的测试:fixture 注入、基于 yield 的清理、表驱动的参数化,以及大作用域带来的陷阱。

9 分钟 · 3 题

在 Kodokon 中打开本课

pytest 建立在两个强有力的想法之上:按约定发现测试(test_*.py 文件、test_* 函数),以及朴素的 assert - 它的内省机制会在失败时显示两侧的实际值,再也不用背诵 assertEqual 和它那三十种变体。有两件工具值得马上认识:pytest.raises 用来检查异常确实被抛出,pytest.approx 用来比较浮点数,免得和二进制舍入较劲。

PYTHON
import pytest

def divide(a: float, b: float) -> float:
    return a / b

def test_nominal() -> None:
    assert divide(8, 4) == 2.0

def test_zero_division() -> None:
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)

def test_floats() -> None:
    assert 0.1 + 0.2 == pytest.approx(0.3)
如果不用 approx,在二进制浮点数下 0.1 + 0.2 == 0.3 是假的。

Fixture 就是 pytest 的依赖注入:只要声明一个与某个 fixture 同名的参数,pytest 就会构造它并注入进来。yield 把准备工作和清理工作分开 - 它之后的代码即使测试失败也会运行scope 参数(默认是 function,还有 modulesession)控制着对象的生命周期。把共享的 fixture 放进 conftest.py:它们就能在整个文件夹里使用,连一条 import 都不用写。

PYTHON
import sqlite3

import pytest

@pytest.fixture
def db():
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (name TEXT)")
    yield conn
    conn.close()

def test_insert(db) -> None:
    db.execute("INSERT INTO users VALUES ('Ada')")
    query = "SELECT name FROM users"
    rows = db.execute(query).fetchall()
    assert rows == [("Ada",)]
无论测试通过还是失败,conn.close() 都会在测试之后运行。

@pytest.mark.parametrize 把一个测试变成一张用例表:每个元组都成为一个独立的测试,可以单独命名和筛选。叠加多个 parametrize 装饰器,就能得到这些参数的笛卡尔积。请通过 pytest.param(..., id='...') 给你的用例起可读的 id - 一份失败报告应该无需打开源文件就能看懂。

PYTHON
import pytest

def slugify(text: str) -> str:
    return "-".join(text.lower().split())

@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        ("Hello World", "hello-world"),
        ("  Python  rocks ", "python-rocks"),
        pytest.param("", "", id="empty"),
    ],
)
def test_slugify(raw: str, expected: str) -> None:
    assert slugify(raw) == expected
三个用例,在报告里就是三个各自独立的测试。
BASH
pytest -x --lf
pytest -k "slugify and not empty"
pytest -q --durations=5
--lf 只重跑失败的测试;--durations 揭示出那些慢测试。

知识检测

确认你已牢记本课的重点内容。

  1. pytest 是怎么知道该给一个测试提供哪个 fixture 的?
    • 靠放在测试上的 @inject 装饰器
    • 靠 fixture 在文件中声明的顺序
    • 靠参数的类型注解
    • 靠参数的名字:如果它匹配一个可见的 fixture(模块内或 conftest.py 中),pytest 就会构造它并注入进来
  2. 在一个 fixture 里,放在 yield 之后的代码有什么用?
    • 那是清理代码:它会在测试之后运行,即使测试失败了也一样
    • 它永远不会运行,yield 就是 fixture 的终点
    • 如果准备工作失败,它会提供一个后备值
    • 它只在测试失败时才运行
  3. pytest --lf 这条命令做什么?
    • 它列出项目中可用的 fixture
    • 它只重跑上一次运行中失败的测试
    • 它在遇到第一个失败时停止本次测试会话