输入关键词开始搜索

pytest — Python 测试框架

基础用法

# test_math.py
def test_addition():
    assert 1 + 1 == 2

def test_division():
    assert 10 / 2 == 5

def test_raises():
    import pytest
    with pytest.raises(ZeroDivisionError):
        1 / 0

def test_approx():
    assert 3.14 == pytest.approx(3.14159, rel=1e-2)  # 近似比较
pytest                          # 运行所有测试
pytest test_math.py             # 指定文件
pytest -k "addition"            # 按名称过滤
pytest -v                       # 详细输出
pytest -x                       # 首次失败即停止
pytest --lf                     # 只运行上次失败的
pytest --maxfail=3               # 3 个失败后停止

fixture — 测试夹具

import pytest

@pytest.fixture
def db():
    """创建测试数据库"""
    conn = create_test_db()
    yield conn          # 测试用例拿到 conn
    conn.close()        # 测试后清理

def test_insert(db):
    db.execute("INSERT INTO t VALUES (1)")
    assert db.fetchone() == (1,)

# scope 控制生命周期
@pytest.fixture(scope="module")   # 整个模块共享
def expensive_resource(): ...

@pytest.fixture(scope="session")  # 整个测试会话共享
def global_config(): ...

# 自动使用(不需传参)
@pytest.fixture(autouse=True)
def reset_counter():
    counter.reset()

参数化

@pytest.mark.parametrize("hex_str,expected", [
    ("00", 0),
    ("0A", 10),
    ("FF", 255),
    ("A5", 165),
])
def test_hex_to_int(hex_str, expected):
    assert hex_to_int(hex_str) == expected

# 组合参数化
@pytest.mark.parametrize("a", [1, 2])
@pytest.mark.parametrize("b", [10, 20])
def test_combine(a, b):
    ...  # 生成 4 个用例: (1,10)(1,20)(2,10)(2,20)

# 从 JSON 加载测试数据
import json
def load_cases():
    with open("testdata/cases.json") as f:
        return json.load(f)

@pytest.mark.parametrize("case", load_cases())
def test_from_json(case):
    assert process(case["input"]) == case["expected"]

mock

from unittest.mock import Mock, patch, MagicMock

# 替换函数
@patch("mymodule.requests.get")
def test_fetch(mock_get):
    mock_get.return_value.status_code = 200
    mock_get.return_value.json.return_value = {"key": "val"}

    result = fetch_data("http://api")

    assert result == {"key": "val"}
    mock_get.assert_called_once_with("http://api")

# 替换对象方法
def test_service():
    mock_db = Mock()
    mock_db.query.return_value = [1, 2, 3]
    svc = Service(mock_db)
    assert svc.get_ids() == [1, 2, 3]

# 验证调用
mock.assert_called_with(arg1, arg2)
mock.assert_called_once()
mock.assert_not_called()
mock.assert_any_call(arg)

conftest.py — 共享配置

# tests/conftest.py — 自动被同级及子目录测试加载
import pytest

@pytest.fixture(scope="session")
def app():
    """创建应用实例,整个测试会话共享"""
    app = create_app(test_mode=True)
    yield app
    app.shutdown()

def pytest_configure(config):
    config.addinivalue_line("markers", "slow: marks tests as slow")

# 命令行选项
def pytest_addoption(parser):
    parser.addoption("--api-url", default="http://localhost")

常用插件

pip install pytest-cov        # 覆盖率
pip install pytest-xdist      # 并行运行(-n auto)
pip install pytest-timeout    # 超时控制
pip install pytest-mock       # 更简洁的 mock

pytest --cov=src --cov-report=html    # 覆盖率 HTML 报告
pytest -n auto                        # 并行(CPU 核数个 worker)
pytest --timeout=10                   # 单个用例 10 秒超时

pytest vs unittest

# unittest 风格(旧)
import unittest
class TestMath(unittest.TestCase):
    def test_add(self):
        self.assertEqual(1 + 1, 2)

# pytest 风格(推荐)
def test_add():
    assert 1 + 1 == 2

CI 集成

# .github/workflows/test.yml
- name: Run tests
  run: |
    pip install pytest pytest-cov
    pytest --cov=src --cov-report=xml --junitxml=report.xml

- name: Upload coverage
  uses: codecov/codecov-action@v4