Skip to content

pytest — 测试框架

pytest 是 Python 最流行的测试框架,简洁的语法和强大的插件生态让测试变得愉快。

安装

bash
pip install pytest pytest-cov pytest-asyncio

基础用法

python
# tests/test_math.py

def add(a, b):
    return a + b

# pytest 自动发现 test_ 开头的函数
def test_add_integers():
    assert add(1, 2) == 3

def test_add_floats():
    assert add(1.1, 2.2) == pytest.approx(3.3)  # 浮点数比较

def test_add_strings():
    assert add("hello", " world") == "hello world"
bash
pytest                    # 运行所有测试
pytest tests/test_math.py # 运行指定文件
pytest -v                 # 详细输出
pytest -k "add"           # 只运行名称含 "add" 的测试
pytest --tb=short         # 简短错误信息

Fixture — 测试夹具

python
import pytest

# 函数级 fixture(每个测试函数运行一次)
@pytest.fixture
def sample_user():
    return {"id": 1, "name": "Alice", "email": "alice@example.com"}

# 模块级 fixture(整个模块只运行一次)
@pytest.fixture(scope="module")
def db_connection():
    conn = create_test_db()
    yield conn          # yield 前是 setup,yield 后是 teardown
    conn.close()

# 会话级 fixture(整个测试会话只运行一次)
@pytest.fixture(scope="session")
def app_config():
    return {"debug": True, "db_url": "sqlite:///:memory:"}

def test_user_name(sample_user):
    assert sample_user["name"] == "Alice"

def test_user_email(sample_user):
    assert "@" in sample_user["email"]

参数化测试

python
import pytest

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

# 参数化 fixture
@pytest.fixture(params=["sqlite", "postgresql"])
def database(request):
    db = create_db(request.param)
    yield db
    db.cleanup()

def test_insert(database):
    # 对每种数据库都运行
    database.insert({"key": "value"})
    assert database.get("key") == "value"

异常测试

python
import pytest

def divide(a, b):
    if b == 0:
        raise ZeroDivisionError("除数不能为零")
    return a / b

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

def test_divide_by_zero_type():
    with pytest.raises(ZeroDivisionError) as exc_info:
        divide(10, 0)
    assert "除数不能为零" in str(exc_info.value)

Mock — 模拟外部依赖

python
from unittest.mock import Mock, patch, AsyncMock
import pytest

# patch 装饰器
@patch("mymodule.requests.get")
def test_fetch_user(mock_get):
    mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}
    mock_get.return_value.status_code = 200

    result = fetch_user(1)
    assert result["name"] == "Alice"
    mock_get.assert_called_once_with("https://api.example.com/users/1")

# patch 上下文管理器
def test_send_email():
    with patch("mymodule.smtplib.SMTP") as mock_smtp:
        send_welcome_email("user@example.com")
        mock_smtp.return_value.__enter__.return_value.sendmail.assert_called_once()

# pytest-mock 插件(更简洁)
def test_with_mocker(mocker):
    mock_db = mocker.patch("mymodule.get_db")
    mock_db.return_value.query.return_value = [{"id": 1}]

    result = get_users()
    assert len(result) == 1

异步测试

python
import pytest
import asyncio

# pytest.ini 或 pyproject.toml 中配置:
# [tool.pytest.ini_options]
# asyncio_mode = "auto"

@pytest.mark.asyncio
async def test_async_function():
    result = await some_async_function()
    assert result == "expected"

# 异步 fixture
@pytest.fixture
async def async_client():
    async with httpx.AsyncClient(app=app, base_url="http://test") as client:
        yield client

@pytest.mark.asyncio
async def test_api_endpoint(async_client):
    response = await async_client.get("/users/1")
    assert response.status_code == 200

FastAPI 测试

python
from fastapi.testclient import TestClient
import pytest

from app.main import app

@pytest.fixture
def client():
    with TestClient(app) as c:
        yield c

def test_read_root(client):
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello, FastAPI!"}

def test_create_user(client):
    payload = {"name": "Alice", "email": "alice@example.com", "age": 30}
    response = client.post("/users/", json=payload)
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Alice"
    assert "id" in data

覆盖率报告

bash
pytest --cov=src --cov-report=html --cov-report=term-missing

# 生成 HTML 报告
open htmlcov/index.html
toml
# pyproject.toml
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/migrations/*"]

[tool.coverage.report]
fail_under = 80   # 覆盖率低于 80% 则失败

测试最佳实践

  • 测试文件放在 tests/ 目录,镜像 src/ 结构
  • 每个测试只测一件事
  • 测试名称描述行为:test_create_user_returns_201
  • 用 fixture 管理测试数据,避免重复
  • Mock 外部依赖(HTTP、数据库、文件系统)

本站内容由 褚成志 整理编写,仅供学习参考