test-runner
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePython 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 () for FastAPI endpoint testing.
AsyncClient使用pytest作为所有Python项目的主要测试框架。结合pytest-asyncio以支持异步测试,pytest-cov用于覆盖率报告,以及httpx()用于FastAPI端点测试。
AsyncClientRunning Tests
运行测试
Basic Test Execution
基础测试执行
bash
undefinedbash
undefinedRun 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"
undefineduv run pytest -m "slow"
undefinedCoverage Reporting
覆盖率报告
bash
undefinedbash
undefinedRun 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
undefineduv run pytest --cov=src --cov-fail-under=80
undefinedUseful Flags
实用参数
bash
undefinedbash
undefinedStop 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
undefineduv run pytest -s
undefinedpytest 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()
undefinedconftest.py
conftest.py
Place shared fixtures in at the appropriate directory level:
conftest.pypython
undefined将共享夹具放在对应目录层级的中:
conftest.pypython
undefinedtests/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
undefinedimport 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
undefinedAsync Tests
异步测试
With in pyproject.toml, async test functions are automatically recognized:
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在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 NoneFastAPI 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 == 404python
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 == 404Mocking
模拟(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()
undefinedParametrize
参数化测试
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_successpython
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_successMarkers
测试标记
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.tomlproject/
├── 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.tomlFixture Scope
夹具作用域
Control how often a fixture is created with the parameter:
scopepython
@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 |
|---|---|---|
| One per test function (default) | Mutable state, per-test isolation |
| One per test class | Shared setup within a test class |
| One per test module | Expensive resources (DB engine, client) |
| One per entire test session | Immutable globals, app factory |
Use the narrowest scope that avoids unnecessary recreation. Async fixtures follow the same scoping rules with .
pytest-asyncio通过参数控制夹具的创建频率:
scopepython
@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) |
|---|---|---|
| 每个测试函数一个(默认) | 可变状态,测试间隔离 |
| 每个测试类一个 | 测试类内的共享初始化 |
| 每个测试模块一个 | 资源密集型组件(数据库引擎、客户端) |
| 整个测试会话周期内一个 | 不可变全局对象,应用工厂 |
使用最窄的作用域以避免不必要的重复创建。异步夹具在下遵循相同的作用域规则。
pytest-asyncioDependency 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 testAlternatively, use fixtures for automatic cleanup:
autousepython
@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() # 关键:防止影响后续测试或者,使用夹具自动清理:
autousepython
@pytest.fixture(autouse=True)
def _cleanup_overrides(app):
yield
app.dependency_overrides.clear()Debugging Tests
调试测试
bash
undefinedbash
undefinedDrop 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
undefinedPYTHONASYNCIODEBUG=1 uv run pytest
undefinedBest Practices
最佳实践
- Name tests clearly: (e.g.,
test_<what>_<condition>_<expected>)test_create_user_empty_name_raises_error - One assertion per test when possible — makes failures easier to diagnose
- Use fixtures for setup/teardown instead of /
setUpmethodstearDown - Test behavior, not implementation — assert on outputs, not internal state
- Use for shared fixtures instead of base test classes
conftest.py - Keep tests fast — mock external services, use in-memory databases for unit tests
- Use context manager for exception assertions
pytest.raises - Use to avoid duplicating test logic
parametrize - Run coverage regularly — aim for meaningful coverage, not 100%
- Separate unit and integration tests with markers or directories
- 清晰命名测试:采用格式(例如:
test_<测试对象>_<条件>_<预期结果>)test_create_user_empty_name_raises_error - 尽可能每个测试一个断言——便于定位失败原因
- 使用夹具进行初始化/清理,而非/
setUp方法tearDown - 测试行为而非实现——断言输出结果,而非内部状态
- **使用**存放共享夹具,而非基测试类
conftest.py - 保持测试快速——模拟外部服务,单元测试使用内存数据库
- **使用**上下文管理器断言异常
pytest.raises - **使用**避免重复测试逻辑
parametrize - 定期运行覆盖率检测——追求有意义的覆盖率,而非100%
- 分离单元测试与集成测试——通过标记或目录区分