>>>PyPathPython 学习站
首页›进阶篇›kp-035
进阶篇 · Advanced lv.3 进阶 kp-035

测试实践(pytest)

前置知识:kp-015、kp-021

1. 一句话定义

pytest 以"普通 assert + fixture 注入 + 参数化"三件套取代 unittest 的样板代码:测试函数写在 test_*.py 里即可运行,断言失败自动展开上下文。

2. 为什么重要

测试是重构的保险带、AI 时代工程协作的契约。Python 社区事实标准是 pytest(Django/FastAPI 官方教程也用它);会写"能维护的测试"是区分玩具项目与工程项目的分水岭。

3. 前置知识

kp-015(异常)、kp-021(虚拟环境)。

4. 核心概念

python
# test_cart.py
import pytest
from cart import Cart, OutOfStock

@pytest.fixture
def cart():                       # fixture:可复用的"测试前提"
    return Cart()

def test_add_item(cart):          # fixture 按名注入
    cart.add("apple", qty=2)
    assert cart.count("apple") == 2

@pytest.mark.parametrize("price, qty, total", [
    (10.0, 2, 20.0),
    (0.5, 4, 2.0),
])
def test_total(cart, price, qty, total):     # 参数化:一函数多用例
    cart.add("x", price=price, qty=qty)
    assert cart.total == total

def test_out_of_stock(cart):
    with pytest.raises(OutOfStock, match="insufficient"):
        cart.add("ghost", qty=99)

  • 发现规则:test_*.py / *_test.py,函数 test_*。
  • 运行:pytest(全量)、pytest test_cart.py::test_total(单个)、-k price(按名过滤)、-x(首败即停)。

5. 原理与机制

bash
uv add --dev pytest ruff
pytest --cov=src --cov-report=term-missing   # 覆盖率(pytest-cov)

  • fixture 生命周期:默认函数级;scope="module"/"session" 提升复用(DB 连接等贵资源)。
  • conftest.py:同目录及以下的 fixture 自动可见——共享前提的标准位置。
  • mock:mocker.patch("mymodule.http_get") 替换外部依赖;原则——mock 你不拥有的东西的边界(网络/时钟/随机),不要 mock 内部实现。

6. 关键事实(模型/图示)

text
测试金字塔(心智):
  多  单元测试(快、隔离、mock 边界)
  中  集成测试(真文件/真小库)
  少  端到端(真环境,冒烟)
命名即文档:test_<行为>_<条件>_<期望>

TDD 节奏(可选方法论):红(写失败测试)→ 绿(最小实现)→ 重构。

7. 直观类比

fixture 是提前布好的舞台(道具按需递给演员);parametrize 是同一剧本的多组台词;mock 是替身演员——危险镜头(真网络/真删除)由替身上,主角逻辑(业务函数)仍然真演。

8. 实例与案例

python
# 测试时间依赖:freezeGun(或 freezegun 库)冻结时钟
from freezegun import freeze_time

@freeze_time("2025-01-01")
def test_expiry():
    ...

9. 常见误区

  1. 测实现而不是行为 —— mock 内部方法、断言调用次数,一重构全红;断言对外可观察的结果。
  2. 没有断言的"测试" —— 只跑不验,绿得毫无意义。
  3. fixture 依赖链过深 —— A 依赖 B 依赖 C…;拆分、命名清晰、必要时直接在测试里构造。
  4. 追求 100% 覆盖率 —— 覆盖率是下限指标不是目标;核心业务逻辑覆盖 > 边角凑数。

10. 自测题

  1. pytest.raises 断言什么?match 参数的作用?
  2. fixture 放 conftest.py 与放测试文件里,可见范围差异?
  3. 什么该 mock、什么不该 mock?
参考答案
  1. 断言代码块抛出指定异常;match 用正则校验异常消息内容。
  2. conftest.py 中对同目录及所有子目录测试可见;文件内 fixture 仅本文件。
  3. mock 外部边界(网络、时钟、随机、文件系统副作用);不 mock 被测函数自身与纯逻辑内部协作。

11. 与其他知识点的关系

  • kp-036 打包:CI 中"测试→构建→发布"流水线的起点。
  • kp-045 工程规范:pre-commit 中 pytest 是常见关卡。
  • kp-015 异常:异常路径是测试重点。

12. 延伸阅读

  • pytest 官方文档:https://docs.pytest.org/
  • 《Effective pytest》系列实践