Python 用 pytest 写测试

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Python 3.13 · pytest 8.3

测试不是为了凑覆盖率,而是让你改代码时敢于下手。这一篇用 pytest 从零搭起可运行的测试。

环境准备

python -m venv .venv
source .venv/bin/activate
python -m pip install pytest==8.3.4
pytest --version

示例软件版本:Python 3.13 + pytest 8.3。

第一个测试

被测代码 calc.py:

def add(a: int, b: int) -> int:
    return a + b

def divide(a: int, b: int) -> float:
    if b == 0:
        raise ValueError("除数不能为 0")
    return a / b

测试 test_calc.py:

from calc import add, divide

def test_add():
    assert add(1, 2) == 3

运行:

pytest

输出要点:

collected 1 item
test_calc.py .                    [100%]
1 passed

pytest 的规则:

  • 测试文件:test_*.py 或 *_test.py
  • 测试函数:以 test_ 开头
  • 断言用裸 assert,不需要 self.assertEqual

断言

def test_list():
    items = [1, 2, 3]
    assert len(items) == 3
    assert 2 in items
    assert items == [1, 2, 3]

失败时 pytest 会打印实际值,定位很快:

E       assert 4 == 3
E        +  where 4 = len([1, 2, 3, 4])

不要写 assert x == True、assert x is True,直接 assert x。

测试异常

import pytest

def test_divide_by_zero():
    with pytest.raises(ValueError, match="除数不能为 0"):
        divide(1, 0)

match 用正则检查异常消息。拿到异常对象细看:

with pytest.raises(ValueError) as exc:
    divide(1, 0)
assert "除数" in str(exc.value)

参数化

同一逻辑用多组输入跑,避免复制粘贴:

@pytest.mark.parametrize(
    ("a", "b", "expected"),
    [
        (1, 2, 3),
        (0, 0, 0),
        (-1, 1, 0),
    ],
)
def test_add_cases(a, b, expected):
    assert add(a, b) == expected

组合多个参数化装饰器可以做笛卡尔积,覆盖更多组合。

fixture:准备与清理

import pytest

@pytest.fixture
def sample_file(tmp_path):
    path = tmp_path / "input.csv"
    path.write_text("name,age\n小明,18\n", encoding="utf-8")
    return path

def test_read(sample_file):
    assert sample_file.exists()
  • fixture 用 @pytest.fixture 定义,测试函数按参数名注入。
  • tmp_path 是内置 fixture:每个测试一个临时目录,自动清理。
  • 需要一次性准备(如数据库连接)用 scope="module" 或 scope="session"。

组织测试

myapp/
├── myapp/
│   ├── __init__.py
│   └── calc.py
└── tests/
    ├── conftest.py        # 共享 fixture
    ├── test_calc.py
    └── test_cli.py

常用命令:

pytest                     # 全部
pytest tests/test_calc.py  # 指定文件
pytest -k "add"            # 按名字匹配
pytest -v                  # 显示每个用例名
pytest -x                  # 第一次失败就停
pytest --tb=short          # 精简栈

覆盖率(可选):

python -m pip install pytest-cov
pytest --cov=myapp --cov-report=term-missing

测试什么、不测试什么

值得测:

  • 业务逻辑与边界:空输入、0、负数、超长字符串。
  • 曾经出过 bug 的地方(回归测试)。
  • 对外接口的行为,而不是内部实现细节。

不值得测:

  • 第三方库本身。
  • 一行 getter/setter。
  • 依赖网络、当前时间、随机数的代码——要用 mock 或注入,而不是真连。

常见错误与注意事项

1. 测试文件名不符合约定

calc_test.py 可以,calc.py 不会被收集(pytest 只收集 test_*.py / *_test.py)。

2. 测试之间互相依赖

pytest 不保证执行顺序(可用 pytest-randomly 强制打乱来暴露问题)。每个测试自己准备数据。

3. 测试有副作用

写真实文件到项目目录、连真实数据库、发真实请求。用 tmp_path、fixture 和 mock 隔离。

4. 断言过于宽松

assert result      # 只要非空就过,什么也证明不了

断言具体值或具体属性。

5. 把测试当成调试脚本

测试要能反复跑、结果稳定。留着 print 调试输出用 pytest -s 临时查看,不要留在提交里。

小结

  • 文件 test_*.py、函数 test_,用裸 assert。
  • 异常用 pytest.raises,多组输入用 parametrize。
  • fixture 负责准备与清理,tmp_path 处理临时文件。
  • 测行为不测实现,隔离外部依赖,保证测试可重复。

下一步