test-runner

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Python Test Runner

Python测试运行器

Overview

概述

Use pytest as the primary test framework for all Python projects. Combine with pytest-asyncio for async test support, pytest-cov for coverage reporting, and httpx (
AsyncClient
) for FastAPI endpoint testing.
使用pytest作为所有Python项目的主要测试框架。结合pytest-asyncio以支持异步测试,pytest-cov用于覆盖率报告,以及httpx
AsyncClient
)用于FastAPI端点测试。

Running Tests

运行测试

Basic Test Execution

基础测试执行

bash
undefined
bash
undefined

Run all tests

Run all tests

uv run pytest
uv run pytest

Run with verbose output

Run with verbose output

uv run pytest -v
uv run pytest -v

Run a specific test file

Run a specific test file

uv run pytest tests/test_users.py
uv run pytest tests/test_users.py

Run a specific test function

Run a specific test function

uv run pytest tests/test_users.py::test_create_user
uv run pytest tests/test_users.py::test_create_user

Run tests matching a keyword expression

Run tests matching a keyword expression

uv run pytest -k "create or delete"
uv run pytest -k "create or delete"

Run tests by marker

Run tests by marker

uv run pytest -m "slow"
undefined
uv run pytest -m "slow"
undefined

Coverage Reporting

覆盖率报告

bash
undefined
bash
undefined

Run with coverage

Run with coverage

uv run pytest --cov=src --cov-report=term-missing
uv run pytest --cov=src --cov-report=term-missing

Generate HTML coverage report

Generate HTML coverage report

uv run pytest --cov=src --cov-report=html
uv run pytest --cov=src --cov-report=html

Fail if coverage is below threshold

Fail if coverage is below threshold

uv run pytest --cov=src --cov-fail-under=80
undefined
uv run pytest --cov=src --cov-fail-under=80
undefined

Useful Flags

实用参数

bash
undefined
bash
undefined

Stop on first failure

Stop on first failure

uv run pytest -x
uv run pytest -x

Stop after N failures

Stop after N failures

uv run pytest --maxfail=3
uv run pytest --maxfail=3

Show local variables in traceback

Show local variables in traceback

uv run pytest -l
uv run pytest -l

Run last failed tests first

Run last failed tests first

uv run pytest --lf
uv run pytest --lf

Run only tests that failed last time

Run only tests that failed last time

uv run pytest --ff
uv run pytest --ff

Parallel execution (requires pytest-xdist)

Parallel execution (requires pytest-xdist)

uv run pytest -n auto
uv run pytest -n auto

Show slowest N tests

Show slowest N tests

uv run pytest --durations=10
uv run pytest --durations=10

Disable output capture (show print statements)

Disable output capture (show print statements)

uv run pytest -s
undefined
uv run pytest -s
undefined

pytest Configuration

pytest配置

pyproject.toml

pyproject.toml

toml
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
python_files = ["test_*.py"]
python_functions = ["test_*"]
markers = [
    "slow: marks tests as slow (deselect with '-m \"not slow\"')",
    "integration: marks tests as integration tests",
]
filterwarnings = [
    "error",
    "ignore::DeprecationWarning",
]
toml
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
python_files = ["test_*.py"]
python_functions = ["test_*"]
markers = [
    "slow: marks tests as slow (deselect with '-m \"not slow\"')",
    "integration: marks tests as integration tests",
]
filterwarnings = [
    "error",
    "ignore::DeprecationWarning",
]

Writing Tests

编写测试

Basic Test Structure

基础测试结构

python
import pytest


def test_addition():
    assert 1 + 1 == 2


def test_exception_raised():
    with pytest.raises(ValueError, match="invalid value"):
        raise ValueError("invalid value")


class TestUserService:
    def test_create_user(self):
        user = create_user(name="Alice")
        assert user.name == "Alice"

    def test_create_user_empty_name(self):
        with pytest.raises(ValueError):
            create_user(name="")
python
import pytest


def test_addition():
    assert 1 + 1 == 2


def test_exception_raised():
    with pytest.raises(ValueError, match="invalid value"):
        raise ValueError("invalid value")


class TestUserService:
    def test_create_user(self):
        user = create_user(name="Alice")
        assert user.name == "Alice"

    def test_create_user_empty_name(self):
        with pytest.raises(ValueError):
            create_user(name="")

Fixtures

测试夹具(Fixtures)

python
import pytest


@pytest.fixture
def sample_user():
    return {"name": "Alice", "email": "alice@example.com"}


@pytest.fixture
def db_session():
    session = create_session()
    yield session
    session.rollback()
    session.close()


@pytest.fixture(autouse=True)
def reset_cache():
    """Automatically reset cache before each test."""
    cache.clear()
    yield
    cache.clear()
python
import pytest


@pytest.fixture
def sample_user():
    return {"name": "Alice", "email": "alice@example.com"}


@pytest.fixture
def db_session():
    session = create_session()
    yield session
    session.rollback()
    session.close()


@pytest.fixture(autouse=True)
def reset_cache():
    """Automatically reset cache before each test."""
    cache.clear()
    yield
    cache.clear()

Fixture with parameters

Fixture with parameters

@pytest.fixture(params=["sqlite", "postgres"]) def database(request): db = create_database(request.param) yield db db.drop()
undefined
@pytest.fixture(params=["sqlite", "postgres"]) def database(request): db = create_database(request.param) yield db db.drop()
undefined

conftest.py

conftest.py

Place shared fixtures in
conftest.py
at the appropriate directory level:
python
undefined
将共享夹具放在对应目录层级的
conftest.py
中:
python
undefined

tests/conftest.py

tests/conftest.py

import pytest from httpx import ASGITransport, AsyncClient
from myapp.main import create_app
@pytest.fixture def app(): return create_app()
@pytest.fixture async def client(app): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac
undefined
import pytest from httpx import ASGITransport, AsyncClient
from myapp.main import create_app
@pytest.fixture def app(): return create_app()
@pytest.fixture async def client(app): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac
undefined

Async Tests

异步测试

With
asyncio_mode = "auto"
in pyproject.toml, async test functions are automatically recognized:
python
async def test_async_endpoint(client):
    response = await client.get("/api/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}


async def test_async_service():
    result = await fetch_data("https://api.example.com/data")
    assert result is not None
在pyproject.toml中设置
asyncio_mode = "auto"
后,异步测试函数会被自动识别:
python
async def test_async_endpoint(client):
    response = await client.get("/api/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}


async def test_async_service():
    result = await fetch_data("https://api.example.com/data")
    assert result is not None

FastAPI Endpoint Testing

FastAPI端点测试

python
import pytest
from httpx import ASGITransport, AsyncClient

from myapp.main import create_app
from myapp.dependencies import get_db


@pytest.fixture
def app():
    app = create_app()
    # Override dependencies for testing
    app.dependency_overrides[get_db] = lambda: fake_db
    yield app
    app.dependency_overrides.clear()


@pytest.fixture
async def client(app):
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac


async def test_create_item(client):
    response = await client.post(
        "/api/items",
        json={"name": "Test Item", "price": 9.99},
    )
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Test Item"


async def test_get_item_not_found(client):
    response = await client.get("/api/items/999")
    assert response.status_code == 404
python
import pytest
from httpx import ASGITransport, AsyncClient

from myapp.main import create_app
from myapp.dependencies import get_db


@pytest.fixture
def app():
    app = create_app()
    # Override dependencies for testing
    app.dependency_overrides[get_db] = lambda: fake_db
    yield app
    app.dependency_overrides.clear()


@pytest.fixture
async def client(app):
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac


async def test_create_item(client):
    response = await client.post(
        "/api/items",
        json={"name": "Test Item", "price": 9.99},
    )
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Test Item"


async def test_get_item_not_found(client):
    response = await client.get("/api/items/999")
    assert response.status_code == 404

Mocking

模拟(Mocking)

python
from unittest.mock import AsyncMock, MagicMock, patch


def test_with_mock():
    mock_service = MagicMock()
    mock_service.get_user.return_value = {"name": "Alice"}

    result = process_user(mock_service, user_id=1)
    mock_service.get_user.assert_called_once_with(1)
    assert result["name"] == "Alice"


async def test_with_async_mock():
    mock_client = AsyncMock()
    mock_client.fetch.return_value = {"data": "value"}

    result = await handler(mock_client)
    mock_client.fetch.assert_awaited_once()


def test_with_patch():
    with patch("myapp.services.external_api.call") as mock_call:
        mock_call.return_value = {"status": "ok"}
        result = my_function()
        assert result["status"] == "ok"
python
from unittest.mock import AsyncMock, MagicMock, patch


def test_with_mock():
    mock_service = MagicMock()
    mock_service.get_user.return_value = {"name": "Alice"}

    result = process_user(mock_service, user_id=1)
    mock_service.get_user.assert_called_once_with(1)
    assert result["name"] == "Alice"


async def test_with_async_mock():
    mock_client = AsyncMock()
    mock_client.fetch.return_value = {"data": "value"}

    result = await handler(mock_client)
    mock_client.fetch.assert_awaited_once()


def test_with_patch():
    with patch("myapp.services.external_api.call") as mock_call:
        mock_call.return_value = {"status": "ok"}
        result = my_function()
        assert result["status"] == "ok"

Patch as decorator

Patch as decorator

@patch("myapp.services.send_email") def test_send_notification(mock_send): mock_send.return_value = True notify_user(user_id=1) mock_send.assert_called_once()
undefined
@patch("myapp.services.send_email") def test_send_notification(mock_send): mock_send.return_value = True notify_user(user_id=1) mock_send.assert_called_once()
undefined

Parametrize

参数化测试

python
import pytest


@pytest.mark.parametrize(
    "input_val, expected",
    [
        (1, 2),
        (2, 4),
        (3, 6),
        (-1, -2),
    ],
)
def test_double(input_val, expected):
    assert double(input_val) == expected


@pytest.mark.parametrize(
    "status_code, is_success",
    [
        (200, True),
        (201, True),
        (400, False),
        (500, False),
    ],
    ids=["ok", "created", "bad-request", "server-error"],
)
def test_is_success_status(status_code, is_success):
    assert is_success_response(status_code) == is_success
python
import pytest


@pytest.mark.parametrize(
    "input_val, expected",
    [
        (1, 2),
        (2, 4),
        (3, 6),
        (-1, -2),
    ],
)
def test_double(input_val, expected):
    assert double(input_val) == expected


@pytest.mark.parametrize(
    "status_code, is_success",
    [
        (200, True),
        (201, True),
        (400, False),
        (500, False),
    ],
    ids=["ok", "created", "bad-request", "server-error"],
)
def test_is_success_status(status_code, is_success):
    assert is_success_response(status_code) == is_success

Markers

测试标记

python
import pytest


@pytest.mark.slow
def test_large_dataset():
    """Takes a long time to run."""
    ...


@pytest.mark.integration
async def test_database_connection():
    """Requires a running database."""
    ...


@pytest.mark.skipif(
    sys.platform == "win32",
    reason="Unix-only test",
)
def test_unix_socket():
    ...


@pytest.mark.xfail(reason="Known bug #123")
def test_known_bug():
    ...
python
import pytest


@pytest.mark.slow
def test_large_dataset():
    """Takes a long time to run."""
    ...


@pytest.mark.integration
async def test_database_connection():
    """Requires a running database."""
    ...


@pytest.mark.skipif(
    sys.platform == "win32",
    reason="Unix-only test",
)
def test_unix_socket():
    ...


@pytest.mark.xfail(reason="Known bug #123")
def test_known_bug():
    ...

Test Project Structure

测试项目结构

project/
├── src/
│   └── myapp/
│       ├── __init__.py
│       ├── main.py
│       ├── services/
│       └── models/
├── tests/
│   ├── __init__.py
│   ├── conftest.py           # Shared fixtures (app, client, db)
│   ├── test_health.py        # Health endpoint tests
│   ├── unit/
│   │   ├── __init__.py
│   │   ├── test_models.py
│   │   └── test_services.py
│   └── integration/
│       ├── __init__.py
│       └── test_api.py
└── pyproject.toml
project/
├── src/
│   └── myapp/
│       ├── __init__.py
│       ├── main.py
│       ├── services/
│       └── models/
├── tests/
│   ├── __init__.py
│   ├── conftest.py           # Shared fixtures (app, client, db)
│   ├── test_health.py        # Health endpoint tests
│   ├── unit/
│   │   ├── __init__.py
│   │   ├── test_models.py
│   │   └── test_services.py
│   └── integration/
│       ├── __init__.py
│       └── test_api.py
└── pyproject.toml

Fixture Scope

夹具作用域

Control how often a fixture is created with the
scope
parameter:
python
@pytest.fixture(scope="function")  # default: new instance per test
def db_session():
    ...

@pytest.fixture(scope="module")    # shared across tests in one module
def db_engine():
    ...

@pytest.fixture(scope="session")   # shared across the entire test session
def app():
    return create_app()
ScopeLifetimeUse Case
function
One per test function (default)Mutable state, per-test isolation
class
One per test classShared setup within a test class
module
One per test moduleExpensive resources (DB engine, client)
session
One per entire test sessionImmutable globals, app factory
Use the narrowest scope that avoids unnecessary recreation. Async fixtures follow the same scoping rules with
pytest-asyncio
.
通过
scope
参数控制夹具的创建频率:
python
@pytest.fixture(scope="function")  # default: new instance per test
def db_session():
    ...

@pytest.fixture(scope="module")    # shared across tests in one module
def db_engine():
    ...

@pytest.fixture(scope="session")   # shared across the entire test session
def app():
    return create_app()
作用域(Scope)生命周期(Lifetime)适用场景(Use Case)
function
每个测试函数一个(默认)可变状态,测试间隔离
class
每个测试类一个测试类内的共享初始化
module
每个测试模块一个资源密集型组件(数据库引擎、客户端)
session
整个测试会话周期内一个不可变全局对象,应用工厂
使用最窄的作用域以避免不必要的重复创建。异步夹具在
pytest-asyncio
下遵循相同的作用域规则。

Dependency Override Cleanup

依赖项覆盖清理

When overriding FastAPI dependencies in tests, always clean up to prevent test pollution:
python
@pytest.fixture
def app():
    app = create_app()
    app.dependency_overrides[get_db] = lambda: fake_db
    yield app
    app.dependency_overrides.clear()  # CRITICAL: prevent leaking to next test
Alternatively, use
autouse
fixtures for automatic cleanup:
python
@pytest.fixture(autouse=True)
def _cleanup_overrides(app):
    yield
    app.dependency_overrides.clear()
在测试中覆盖FastAPI依赖项时,务必清理以防止测试污染:
python
@pytest.fixture
def app():
    app = create_app()
    app.dependency_overrides[get_db] = lambda: fake_db
    yield app
    app.dependency_overrides.clear()  # 关键:防止影响后续测试
或者,使用
autouse
夹具自动清理:
python
@pytest.fixture(autouse=True)
def _cleanup_overrides(app):
    yield
    app.dependency_overrides.clear()

Debugging Tests

调试测试

bash
undefined
bash
undefined

Drop into debugger on failure

Drop into debugger on failure

uv run pytest --pdb
uv run pytest --pdb

Drop into debugger at first line of each test

Drop into debugger at first line of each test

uv run pytest --trace
uv run pytest --trace

Show full diff for assertion errors

Show full diff for assertion errors

uv run pytest -vv
uv run pytest -vv

Enable asyncio debug mode

Enable asyncio debug mode

PYTHONASYNCIODEBUG=1 uv run pytest
undefined
PYTHONASYNCIODEBUG=1 uv run pytest
undefined

Best Practices

最佳实践

  1. Name tests clearly:
    test_<what>_<condition>_<expected>
    (e.g.,
    test_create_user_empty_name_raises_error
    )
  2. One assertion per test when possible — makes failures easier to diagnose
  3. Use fixtures for setup/teardown instead of
    setUp
    /
    tearDown
    methods
  4. Test behavior, not implementation — assert on outputs, not internal state
  5. Use
    conftest.py
    for shared fixtures instead of base test classes
  6. Keep tests fast — mock external services, use in-memory databases for unit tests
  7. Use
    pytest.raises
    context manager for exception assertions
  8. Use
    parametrize
    to avoid duplicating test logic
  9. Run coverage regularly — aim for meaningful coverage, not 100%
  10. Separate unit and integration tests with markers or directories
  1. 清晰命名测试:采用
    test_<测试对象>_<条件>_<预期结果>
    格式(例如:
    test_create_user_empty_name_raises_error
  2. 尽可能每个测试一个断言——便于定位失败原因
  3. 使用夹具进行初始化/清理,而非
    setUp
    /
    tearDown
    方法
  4. 测试行为而非实现——断言输出结果,而非内部状态
  5. **使用
    conftest.py
    **存放共享夹具,而非基测试类
  6. 保持测试快速——模拟外部服务,单元测试使用内存数据库
  7. **使用
    pytest.raises
    **上下文管理器断言异常
  8. **使用
    parametrize
    **避免重复测试逻辑
  9. 定期运行覆盖率检测——追求有意义的覆盖率,而非100%
  10. 分离单元测试与集成测试——通过标记或目录区分