Pytest

技术栈
工具链
pytestpython测试单元测试TDD自动化测试

概览

Pytest

Pytest 是 Python 最流行的测试框架,由 Holger Krekel 于 2004 年创建。它以简洁的断言语法和丰富的插件生态取代了标准库 unittest。

解决什么问题

  • 用最少的样板代码编写测试用例
  • 提供 fixture 机制优雅管理测试依赖和资源
  • 参数化测试避免重复代码

关键特性

  • 自动发现测试:以 test_ 开头即可
  • 原生 assert 语句,无需 self.assertEqual
  • @pytest.fixture 依赖注入,支持 scope 控制和 teardown
  • @pytest.mark.parametrize 参数化测试
  • 强大的插件生态:pytest-cov、pytest-xdist、pytest-mock 等
  • 兼容 unittest 和 nose 测试

安装

环境准备

  • 操作系统: Windows / macOS / Linux 均可
  • Python 版本: 3.7 及以上(推荐 3.9+)
  • 前置依赖: iniconfigpackagingpluggy(自动安装)

安装命令

# 基础安装
pip install pytest

# 安装常用插件全家桶
pip install pytest pytest-cov pytest-xdist pytest-mock pytest-timeout

# 验证安装
pytest --version

常见安装问题

Q: 与系统自带的 pytest 冲突

# 使用虚拟环境隔离
python -m venv venv
source venv/bin/activate
pip install pytest

Q: 运行 pytest 提示命令找不到

python -m pip install pytest
python -m pytest  # 通过 Python 模块运行

Q: 插件版本兼容问题

  • 查看当前环境所有包:pip list
  • 升级所有测试相关包:pip install --upgrade pytest pytest-cov pytest-mock

Q: VS Code 集成 Pytest
在项目根目录创建 .vscode/settings.json

{
    "python.testing.pytestEnabled": true,
    "python.testing.unittestEnabled": false
}

示例

Pytest Hello World:第一个测试用例

目标

编写一个简单的数学函数,并用 pytest 测试它的各种情况,包括正常输入、边界值和异常。

完整代码

# math_utils.py — 被测试的模块
def divide(a: float, b: float) -> float:
    """安全除法,分母为零时抛出 ValueError"""
    if b == 0:
        raise ValueError("除数不能为零")
    return a / b


def is_prime(n: int) -> bool:
    """判断是否为质数"""
    if n < 2:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True
# test_math_utils.py — 测试文件(必须以 test_ 开头)
import pytest
from math_utils import divide, is_prime


# === 测试 divide 函数 ===
def test_divide_normal():
    assert divide(10, 2) == 5.0
    assert divide(7, 2) == 3.5


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


# === 测试 is_prime 函数(参数化) ===
@pytest.mark.parametrize("n, expected", [
    (1, False),
    (2, True),
    (3, True),
    (4, False),
    (17, True),
    (100, False),
    (97, True),
])
def test_is_prime(n, expected):
    assert is_prime(n) == expected

运行步骤

pytest test_math_utils.py -v

预期输出

test_math_utils.py::test_divide_normal PASSED
test_math_utils.py::test_divide_by_zero PASSED
test_math_utils.py::test_is_prime[1-False] PASSED
test_math_utils.py::test_is_prime[2-True] PASSED
test_math_utils.py::test_is_prime[3-True] PASSED
test_math_utils.py::test_is_prime[4-False] PASSED
test_math_utils.py::test_is_prime[17-True] PASSED
test_math_utils.py::test_is_prime[100-False] PASSED
test_math_utils.py::test_is_prime[97-True] PASSED

======= 9 passed in 0.05s =======

教程

Pytest 测试框架完全指南

背景

测试是软件质量的基石。Pytest 把编写测试这件事从「负担」变成「享受」——它极简的语法让你无需继承 TestCase、无需记忆 30 种 assert 方法,只需写普通的 assert 语句。


第 1 章:Fixture 依赖注入

Fixture 是 Pytest 最核心的概念,用于准备测试所需的资源。

import pytest
import sqlite3

@pytest.fixture(scope="module")
def db():
    """模块级别的数据库连接,所有测试共享"""
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (id INTEGER, name TEXT)")
    conn.execute("INSERT INTO users VALUES (1, 'Alice')")
    yield conn  # 测试执行到这里
    conn.close()  # teardown 清理

def test_query_user(db):
    cursor = db.execute("SELECT name FROM users WHERE id=1")
    assert cursor.fetchone() == ("Alice",)

Fixture Scope

scope 生命周期
function(默认) 每个测试函数
class 每个测试类
module 每个 .py 文件
session 整个 pytest 运行期间

第 2 章:Mock 与 Monkeypatch

# 使用 monkeypatch 替换外部依赖
def test_fetch_data(monkeypatch):
    def mock_get(url, *args, **kwargs):
        class FakeResponse:
            status_code = 200
            def json(self):
                return {"data": "mocked"}
        return FakeResponse()

    monkeypatch.setattr("requests.get", mock_get)
    from myapp import fetch_user_data
    assert fetch_user_data(1) == {"data": "mocked"}

第 3 章:conftest.py 共享 Fixture

# conftest.py — 放在项目根目录,自动被所有测试文件共享
import pytest
from myapp import create_app

@pytest.fixture
def app():
    app = create_app(testing=True)
    yield app

@pytest.fixture
def client(app):
    return app.test_client()

# 任何 test_*.py 都可以直接使用 app 和 client fixture

第 4 章:覆盖率与并行

# 生成覆盖率报告
pytest --cov=myapp --cov-report=html tests/

# 并行执行(需要 pytest-xdist)
pytest -n auto tests/

# 组合使用
pytest -n 4 --cov=myapp --cov-report=term tests/

第 5 章:标记(Mark)与分组

# 给测试打标记
@pytest.mark.slow
def test_heavy_computation():
    pass

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

# 按标记运行
# pytest -m "slow"          # 只运行 slow
# pytest -m "not integration"  # 跳过 integration
# pytest -m "slow and not integration"

pytest.ini 中注册标记:

[pytest]
markers =
    slow: 标记慢速测试
    integration: 标记集成测试

思考题

  1. fixture 的 scope="session"scope="function" 分别适合管理什么类型的资源?
  2. monkeypatch 和 unittest.mock 的区别是什么?什么时候该用哪种?
  3. 如何在 CI/CD 中设置最低覆盖率阈值并阻止低覆盖率合并?