pytest 从零到实战:要想代码好,测试少不了

17 阅读7分钟

测试代码这件事,很多人知道重要,但真正动手写的时候往往不知道从哪里开始。pytest 就是那把能让你"开口说话"的钥匙——它足够简单,让你五分钟内跑起第一个测试;又足够强大,支撑得起大型生产系统的复杂测试需求。这篇教程会带你把核心概念摸透,同时给出贴近真实工程的代码示例。


一、pytest 是什么,为什么选它

Python 生态里的测试框架不止一个,但 pytest 已经成为事实上的行业标准。原因很朴素:

  • 写法极简,用最普通的 assert 语句就能断言,不需要记一堆 self.assertEqual 之类的 API
  • 自动发现测试,只要文件名和函数名符合约定,pytest 自己就能找到并执行
  • 插件生态丰富,超过 1300 个外部插件,覆盖覆盖率统计、并行执行、异步测试等各种场景
  • 与 CI/CD 无缝集成,GitHub Actions、Jenkins、GitLab CI 都能直接用

安装只需一行:

pip install pytest

写一个最简单的测试,新建文件 test_sample.py

def inc(x):
    return x + 1

def test_answer():
    assert inc(3) == 5  # 故意写错,看看报错长什么样

执行 pytest,你会看到 pytest 给出非常清晰的失败报告:

FAILED test_sample.py::test_answer - assert 4 == 5
  +  where 4 = inc(3)

它直接告诉你"期望 5,实际得到 4,来自 inc(3)"——这种断言内省能力是 pytest 的一大亮点。


二、测试发现规则与项目结构

pytest 找测试文件有一套约定,记住这两条就够了:

  • 文件名必须是 test_*.py*_test.py
  • 测试函数必须以 test_ 开头

一个典型的工程项目结构长这样:

my_project/
├── src/
│   └── app/
│       ├── users.py
│       └── orders.py
└── tests/
    ├── conftest.py       ← 共享配置和 fixture
    ├── test_users.py
    └── test_orders.py

源码和测试分开放,是工程实践中的基本素养。conftest.py 是个特殊文件,后面会重点讲。


三、AAA 模式:让测试代码像文章一样好读

好的测试代码有结构感。业界流行一种叫 Arrange–Act–Assert(AAA) 的写法:

def test_login_success():
    # Arrange — 准备数据
    user = User("john", "123")

    # Act — 执行被测逻辑
    result = authenticate(user.username, user.password)

    # Assert — 验证结果
    assert result is True

三段式结构让每个测试的意图一目了然:我准备了什么、做了什么、期望什么。这不是强制要求,但养成这个习惯,你的测试代码会让同事刮目相看。


四、Fixture:测试的"后勤保障部队"

Fixture 是 pytest 最核心、也最有魅力的概念。简单说,它就是可复用的测试前置准备——数据库连接、测试用户、配置对象,这些东西你不想在每个测试里重复写,就用 fixture 封装起来。

基础用法

import pytest

@pytest.fixture
def sample_user():
    return {"name": "Alice", "role": "admin"}

def test_user_role(sample_user):   # 把 fixture 名字作为参数传入
    assert sample_user["role"] == "admin"

pytest 看到函数参数名 sample_user,会自动找到同名 fixture 并注入。不需要手动调用,这就是依赖注入的魔法。

Fixture 的生命周期(scope)

不是所有资源都需要每次测试都重建。pytest 提供四种 scope:

scope创建时机适用场景
function(默认)每个测试函数普通数据对象
class每个测试类类内共享状态
module每个测试文件模块级数据库连接
session整个测试会话全局配置、重量级资源
@pytest.fixture(scope="module")
def db_connection():
    conn = create_fake_db()
    return conn

用 yield 实现自动清理

测试完要"收拾残局",yield 让 setup 和 teardown 写在一起,逻辑清晰:

@pytest.fixture
def db_conn():
    conn = create_db_connection()
    yield conn          # ← 测试在这里运行
    conn.close()        # ← 测试结束后自动执行清理

这比 setUp/tearDown 的写法优雅太多了。


五、conftest.py:跨文件共享 fixture 的秘密武器

conftest.py 是 pytest 的"公共仓库"。放在这个文件里的 fixture,同目录及子目录下的所有测试文件都能直接使用,不需要 import

# tests/conftest.py
import pytest

@pytest.fixture
def auth_token():
    return "Bearer test-token-xyz"
# tests/test_api.py
def test_get_user(auth_token):   # 直接用,不用 import
    headers = {"Authorization": auth_token}
    # ...

大型项目里,conftest.py 可以分层放置——根目录一个,子目录各一个,pytest 会按就近原则查找。


六、参数化测试:一个函数,跑 N 种情况

写测试最怕的就是复制粘贴。参数化(parametrize)让你用一个测试函数覆盖多组输入:

import pytest

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

pytest 会把这个测试展开成 4 个独立用例,分别报告通过或失败。边界值、正常值、异常值一网打尽,覆盖率蹭蹭往上涨。


七、Mock 与 Monkeypatch:隔离外部依赖

真实项目里,代码往往要调用数据库、发 HTTP 请求、读写文件。测试时不可能真的去连数据库,这时候就需要"替身演员"——Mock。

用 unittest.mock 打桩

from unittest.mock import patch

@patch("app.email.send_email")
def test_send_notification(mock_send):
    mock_send.return_value = True
    result = send_notification("user@example.com")
    assert result is True
    mock_send.assert_called_once()  # 验证确实被调用了

pytest 内置的 monkeypatch

monkeypatch 更轻量,特别适合替换环境变量、函数或对象属性:

def test_debug_mode(monkeypatch):
    monkeypatch.setenv("DEBUG", "true")
    assert os.environ["DEBUG"] == "true"
# 测试结束后,环境变量自动恢复原样

Mock、Stub、Spy 的区别

这三个概念经常被混淆,一张表说清楚:

概念作用典型场景
Mock完全替换依赖对象无法运行真实依赖时
Stub只提供预设返回值只关心输出,不关心行为
Spy包裹真实逻辑并记录调用验证函数是否被调用

八、Markers:给测试贴标签,按需执行

随着测试越来越多,你会希望能分组运行——只跑快速的单元测试,或者只跑集成测试。Markers 就是这个用途:

@pytest.mark.slow
def test_heavy_computation():
    ...

@pytest.mark.integration
def test_database_write():
    ...

@pytest.mark.skip(reason="功能尚未实现")
def test_future_feature():
    ...

# 条件跳过
@pytest.mark.skipif(sys.platform == "win32", reason="不支持 Windows")
def test_unix_only():
    ...

执行时按标签过滤:

pytest -m "not slow"        # 跳过慢测试
pytest -m "integration"     # 只跑集成测试

自定义 marker 需要在 pytest.ini 里注册,避免警告:

[pytest]
markers =
    slow: 标记为耗时测试
    integration: 标记为集成测试

九、异常测试与内置工具 fixture

测试异常

import pytest

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        result = 1 / 0

# 还可以验证错误信息
def test_invalid_input():
    with pytest.raises(ValueError) as exc_info:
        parse_age(-1)
    assert "must be positive" in str(exc_info.value)

几个好用的内置 fixture

# 捕获 print 输出
def test_output(capsys):
    print("hello pytest")
    captured = capsys.readouterr()
    assert "hello" in captured.out

# 捕获日志
def test_logging(caplog):
    import logging
    logging.getLogger().info("服务启动")
    assert "服务启动" in caplog.text

# 临时文件目录(测试结束自动删除)
def test_file_write(tmp_path):
    f = tmp_path / "result.txt"
    f.write_text("data")
    assert f.read_text() == "data"

十、常用插件生态

pytest 的插件体系是它长盛不衰的核心原因之一。下面是工程实践中最常装的几个:

插件功能安装命令
pytest-cov代码覆盖率报告pip install pytest-cov
pytest-xdist多核并行执行测试pip install pytest-xdist
pytest-mock更简洁的 mock 接口pip install pytest-mock
pytest-asyncio支持 async/await 测试pip install pytest-asyncio
pytest-timeout检测卡死的测试pip install pytest-timeout
pytest-randomly随机化执行顺序,发现隐藏依赖pip install pytest-randomly

并行执行示例,大型项目能显著提速:

pytest -n auto   # 自动使用所有 CPU 核心

异步测试示例(FastAPI 项目必备):

import pytest

@pytest.mark.asyncio
async def test_async_fetch():
    result = await fetch_data_from_api()
    assert result["status"] == "ok"

覆盖率报告:

pytest --cov=src --cov-report=html

十一、核心概念关系图

graph TD
    A[pytest 测试套件] --> B[测试文件 test_*.py]
    A --> C[conftest.py]
    B --> D[测试函数 test_xxx]
    B --> E[测试类 TestXxx]
    C --> F[共享 Fixture]
    F --> G[scope: function]
    F --> H[scope: module]
    F --> I[scope: session]
    D --> J[Markers 标记]
    D --> K[parametrize 参数化]
    D --> L[Mock / Monkeypatch]
    J --> M[skip / slow / integration]
    L --> N[unittest.mock.patch]
    L --> O[monkeypatch 内置]

写在最后

学 pytest 最好的方式,是在一个真实项目里从第一个 test_ 函数开始写起。先掌握 fixture 和 parametrize,这两个工具能解决 80% 的日常测试需求;再慢慢引入 mock 和插件,随着项目规模增长自然就知道什么时候需要什么。

测试不是负担,是你对自己代码质量的承诺。写好测试的代码,重构起来才真的有底气。