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

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


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

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

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

安装只需一行:

复制代码
pip install pytest

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

python 复制代码
def inc(x):
    return x + 1

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

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

ini 复制代码
FAILED test_sample.py::test_answer - assert 4 == 5
  +  where 4 = inc(3)

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


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

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

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

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

css 复制代码
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) 的写法:

ini 复制代码
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 封装起来。

基础用法

python 复制代码
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 整个测试会话 全局配置、重量级资源
ini 复制代码
@pytest.fixture(scope="module")
def db_connection():
    conn = create_fake_db()
    return conn

用 yield 实现自动清理

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

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

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


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

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

python 复制代码
# tests/conftest.py
import pytest

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

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


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

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

python 复制代码
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 打桩

python 复制代码
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 更轻量,特别适合替换环境变量、函数或对象属性:

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

Mock、Stub、Spy 的区别

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

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

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

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

less 复制代码
@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():
    ...

执行时按标签过滤:

bash 复制代码
pytest -m "not slow"        # 跳过慢测试
pytest -m "integration"     # 只跑集成测试

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

ini 复制代码
[pytest]
markers =
    slow: 标记为耗时测试
    integration: 标记为集成测试

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

测试异常

python 复制代码
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

python 复制代码
# 捕获 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

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

arduino 复制代码
pytest -n auto   # 自动使用所有 CPU 核心

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

python 复制代码
import pytest

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

覆盖率报告:

css 复制代码
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 和插件,随着项目规模增长自然就知道什么时候需要什么。

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

相关推荐
K神1 小时前
Spring gRPC集成Spring Security OAuth2
后端·grpc
Ai拆代码的曹操1 小时前
Dubbo 直连模式翻车现场:2.7.x 调 3.x 的序列化大坑
后端
Chenyiax1 小时前
AC 自动机原理:把一组关键词编译成一次线性扫描
后端
吃糖的小孩1 小时前
# RootGraph v1.5 收工:我给 QQ 机器人补上了聊天运行时的“黑匣子”
python
程序员cxuan1 小时前
读懂 Claude Code 架构分析系列,第二篇:Claude Code 是怎样启动的?
人工智能·后端·程序员
用户40966601317511 小时前
MySQL 慢查询优化实战:从 EXPLAIN 到索引设计
后端
hangyuekejiGEO2 小时前
临沂GEO技术服务方案对比分析
大数据·人工智能·python
从零开始的代码生活_2 小时前
Linux epoll 多路转接详解
linux·运维·网络·后端·tcp/ip·计算机网络·php
IT_陈寒2 小时前
Vue的响应式更新把我坑惨了,原来是这个原因
前端·人工智能·后端