SuperAPI AI Directory

python-pytest

Testing & quality

View repository

Updated 10-16 09:44

About this skill

Python pytest testing specifications

Core Principles

  • Tests should befast, independent, repeatable, self-validating(FIRST principles)
  • Each test function verifies only one behavior
  • Test names should clearly express the behavior under test and the expected result
  • Target test coverage:Core logic ≥ 80%

Test Type Decision Tree

What to test Recommended approach
Return value of a function/method Basic unit test, AAA pattern
Function behavior (calls, side effects) Mock to verify function calls and arguments
External dependencies such as databases/APIs Fixtures + Mock external services
Async code pytest-asyncio, configure asyncio_mode = "auto"
Multiple input combinations @pytest.mark.parametrize parameterized tests
Exception cases pytest.raises
Shared test data or resources Fixtures (choose by scope)

Directory Structure

project/
├── src/
│   └── mypackage/
│       ├── __init__.py
│       ├── calculator.py
│       └── user_service.py
└── tests/
    ├── conftest.py          # 共享 fixture
    ├── unit/                # 单元测试(快,无外部依赖)
    ├── integration/         # 集成测试(测试环境,连真实依赖)
    └── production/          # 生产冒烟(只读、健康检查)

Note:tests/ directories do not require __init__.py files.

Execute by layer:

pytest tests/unit/                    # 快速验证,CI 必跑
pytest tests/integration/             # 连真实依赖,较慢
pytest tests/production/ -v           # 发布后冒烟
pytest -m "not production"            # 排除生产测试

Test File Naming

  • Test files start with test_ :test_calculator.py
  • Test functions start with test_ :def test_add_returns_correct_sum()
  • Helper files do not use the test_ prefix:helpers.py、factories.py

Test Function Naming Conventions

Follow the test_<功能>_<场景>_<预期结果> pattern:

# ✅ 清晰的命名
def test_divide_by_zero_raises_value_error():
def test_create_user_with_valid_data_returns_user():
def test_login_with_invalid_password_returns_401():

# ❌ 模糊的命名
def test_divide():
def test_user():
def test_login2():

Basic Test Structure (AAA Pattern)

def test_calculate_discount_returns_correct_price():
    # Arrange(准备)
    price = 100.0
    rate = 0.2

    # Act(执行)
    result = calculate_discount(price, rate)

    # Assert(验证)
    assert result == 80.0

Using pytest fixtures

Define a fixture

# tests/conftest.py
import pytest
from mypackage.database import Database

@pytest.fixture
def db():
    """提供测试数据库连接,测试结束后自动清理。"""
    database = Database(":memory:")
    database.create_tables()
    yield database
    database.close()

@pytest.fixture
def sample_user():
    return {"id": 1, "name": "张三", "email": "zhang@example.com"}

Use a fixture

def test_save_user_persists_to_database(db, sample_user):
    db.save_user(sample_user)
    retrieved = db.get_user(sample_user["id"])
    assert retrieved["name"] == sample_user["name"]

fixture scope

@pytest.fixture(scope="session")   # 整个测试会话共享一次
@pytest.fixture(scope="module")    # 每个模块共享一次
@pytest.fixture(scope="class")     # 每个测试类共享一次
@pytest.fixture(scope="function")  # 默认,每个测试函数独立

Parameterized Tests

Use @pytest.mark.parametrize to avoid duplicating test code:

import pytest

@pytest.mark.parametrize("price,rate,expected", [
    (100.0, 0.0, 100.0),   # 无折扣
    (100.0, 0.5, 50.0),    # 五折
    (100.0, 1.0, 0.0),     # 全额折扣
    (0.0, 0.3, 0.0),       # 零价格
])
def test_calculate_discount(price, rate, expected):
    assert calculate_discount(price, rate) == expected


@pytest.mark.parametrize("invalid_rate", [-0.1, 1.1, 2.0])
def test_calculate_discount_with_invalid_rate_raises(invalid_rate):
    with pytest.raises(ValueError, match="折扣率必须在"):
        calculate_discount(100.0, invalid_rate)

Exception Testing

def test_divide_by_zero_raises_zero_division_error():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

# 验证异常消息
def test_invalid_email_raises_with_message():
    with pytest.raises(ValueError, match="邮箱格式不正确"):
        validate_email("not-an-email")

# 捕获并检查异常对象
def test_custom_exception_has_correct_code():
    with pytest.raises(AppError) as exc_info:
        raise_app_error(code=404)
    assert exc_info.value.code == 404

Mock and Patch

monkeypatch vs pytest-mock

monkeypatch — simple replacement, suitable for modifying environment variables, configuration, etc.:

def test_with_monkeypatch(monkeypatch):
    monkeypatch.setenv("API_KEY", "test_key")
    monkeypatch.setattr("module.function", mock_function)

pytest-mock (mocker) — powerful mock functionality, suitable for verifying calls:

def test_with_mocker(mocker):
    mock_func = mocker.patch("module.function")
    mock_func.return_value = "result"
    mock_func.assert_called_with("args")

Patch Strategy

✅ Correct: patch where it is used (patch the path referenced by the code under test)

mocker.patch("myapp.utils.helper.function")  # myapp 使用 function

❌ Incorrect: patch where it is defined

from myapp.utils import helper
mocker.patch("helper.function")  # 不会生效

Common Mock Scenarios

from unittest.mock import MagicMock, patch

def test_send_email_calls_smtp_once():
    with patch("mypackage.email.smtplib.SMTP") as mock_smtp:
        mock_instance = mock_smtp.return_value.__enter__.return_value
        send_welcome_email("user@example.com")
        mock_instance.sendmail.assert_called_once()


def test_get_user_calls_api_with_correct_id():
    mock_client = MagicMock()
    mock_client.get.return_value = {"id": 1, "name": "李四"}

    service = UserService(client=mock_client)
    user = service.get_user(1)

    mock_client.get.assert_called_once_with("/users/1")
    assert user["name"] == "李四"

Integration Tests (Internal Processes)

Integration tests verify complete business processes chaining multiple modules together. The core difference from unit tests lies in the Mock scope:

Unit Test Integration Test
Mock scope Mock all external dependencies Mock only true external boundaries (DB, HTTP, files)
Test granularity Single function/method Complete business process
Speed Extremely fast Slower, acceptable

Mock only external boundaries; internal logic runs real code

# ✅ 内部流程全部走真实代码,只隔离外部 I/O
def test_factor_compute_pipeline(mocker, sample_data):
    mocker.patch("myapp.data.get_klines", return_value=sample_data)  # 只 Mock 外部边界

    engine = TimeSeriesEngine(data=sample_data, factor=factor)
    result = engine.results

    assert not result.empty
    assert "F#SMA60#DEFAULT" in result.columns

Organize by process stage, not by module

# tests/integration/test_factor_pipeline.py

def test_factor_compute_returns_valid_output(sample_data):
    """阶段1:因子计算"""
    engine = TimeSeriesEngine(data=sample_data, factor=factor)
    assert not engine.results.empty

def test_factor_eval_ic_is_reasonable(factor_results):
    """阶段2:因子评估"""
    ic = calc_ic(factor_results)
    assert abs(ic) > 0.01

def test_model_weights_sum_to_one(factor_results):
    """阶段3:策略建模"""
    model = CSSorting(factor_results, factor="F#SMA60#DEFAULT", top_pct=0.2)
    weights = model.run()
    assert abs(weights["weight"].sum()) <= 1.0

fixture scope selection

Scenario Recommended scope
Generate test data (stateless) module or session
Stateful objects (such as engine instances) function
Database connections session

Common pitfalls

  • Too many Mocks → equivalent to not testing: If multiple internal modules are all Mocked, the process is not truly connected end-to-end
  • Implicit dependencies between tests: Test B cannot depend on the side effects of Test A; each test case should be able to run independently

Principles of testing in production

pytest can be used for production environment verification, but scenarios must be strictly distinguished.

What can be done (safe scenarios)

Scenario Description
Smoke testing Verify after release that the service starts and key interfaces are available
Health checks Verify environment variables, certificates, and connectivity to dependent services
Traffic replay Replicate production traffic to compare the old and new versions without affecting users
Performance benchmarks Verify that the new version is not slower than the old version and has no memory leaks

Absolutely prohibited

  • Test cases with write operations (creating data, deleting data, changing state) → pollute the production database
  • High-concurrency testing → overwhelm the production service
  • Ordinary unit tests run directly in production (many unit tests clear tables and Mock external dependencies)

Production smoke test example

@pytest.mark.production
def test_api_health(client):
    resp = client.get("/health")
    assert resp.status_code == 200
    assert resp.json()["status"] == "ok"

@pytest.mark.production
def test_api_response_time(client):
    import time
    start = time.time()
    client.get("/api/user/info")
    assert time.time() - start < 0.3  # 响应时间断言

Mandatory rules for production testing: read-only, no dirty data creation, single-threaded serial execution, and no truncate/drop。

Asynchronous testing

import pytest

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

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


async def test_async_operation_with_fixture(db):
    await db.async_save({"key": "value"})
    result = await db.async_get("key")
    assert result == "value"

Markers

# 标记慢速测试
@pytest.mark.slow
def test_large_data_processing():
    ...

# 标记需要网络的测试
@pytest.mark.network
def test_external_api_integration():
    ...

# 跳过测试
@pytest.mark.skip(reason="功能尚未实现")
def test_future_feature():
    ...

# 条件跳过
@pytest.mark.skipif(sys.platform == "win32", reason="不支持 Windows")
def test_unix_only_feature():
    ...

In pyproject.toml register custom markers:

[tool.pytest.ini_options]
markers = [
    "slow: 标记为慢速测试,可用 -m 'not slow' 跳过",
    "network: 需要网络连接的集成测试",
]

pytest configuration (pyproject.toml)

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = "-v --tb=short"

[tool.coverage.run]
source = ["src"]
omit = ["tests/*", "**/__init__.py"]

[tool.coverage.report]
fail_under = 80
show_missing = true

Run command

# 运行所有测试
pytest

# 运行指定文件
pytest tests/test_calculator.py

# 运行指定测试函数
pytest tests/test_calculator.py::test_add_returns_correct_sum

# 按标记运行
pytest -m slow
pytest -m "not slow"

# 带覆盖率报告
pytest --cov=src --cov-report=term-missing

# 失败时立即停止
pytest -x

# 并行执行(需安装 pytest-xdist)
pytest -n auto

Workflow

Steps for writing new tests

  1. Determine the test type — refer to the decision tree above and choose unit testing, integration testing, or asynchronous testing
  2. Create the test file — test_<module>.py
  3. Write the test function — follow the AAA pattern
  4. Add necessary fixtures — reuse test data and resources
  5. Mock external dependencies — keep tests isolated and fast
  6. Verify coverage — pytest --cov
  7. Run the tests — pytest -v

Diagnosing test failures

  1. View detailed output — pytest -v or pytest -vv
  2. Print debugging information — use print()in tests (remove before committing)
  3. Run a single test — pytest tests/test_file.py::test_function
  4. View the full error stack — pytest --tb=long

Detailed reference documentation

This skill includes the following detailed reference documentation (located in the references/ directory):

fixtures.md

Complete Fixtures guide: basic fixtures, scopes, parameterization, dependencies, conftest.py, built-in fixtures (tmp_path, capsys, caplog, monkeypatch), Async fixtures

mocking.md

Complete Mocking and Patching guide: monkeypatch usage, pytest-mock usage, Mock object configuration and verification, common mock patterns (API, database, filesystem), Patch strategies, common pitfalls

async-testing.md

Complete Guide to Async Testing: pytest-asyncio Configuration (auto/strict modes), Basic Async Testing, Async Fixtures, Loop Scope Control, Mocking Async Operations

patterns.md

Common Testing Patterns and Examples: Test Organization, Parametrized Testing, Exception Testing, Marking and Skipping, Test Configuration, Data-Driven Testing, Integration Testing Patterns, Boundary Condition Testing

best-practices.md

Testing Best Practices and Conventions: FIRST Principles, AAA/Given-When-Then Pattern, Coverage Configuration, Fixture/Mock/Assertion Best Practices, Performance Considerations, CI/CD Integration

Best Practices

  • Run only relevant tests, rather than the entire test suite; execute by module or feature scope to save time
  • Read existing tests before writing new tests, understand the project's testing style and idiomatic patterns, and maintain consistency
  • Use print to assist debugging, and delete it promptly after completion; do not leave debug output in the final committed code
  • Resolve all errors and warnings before committing; commit only after passing ruff and basedpyright checks

Test Quality Checklist

Before committing test code, confirm:

  • Test names clearly describe the behavior under test
  • Follow the AAA (Arrange-Act-Assert) pattern
  • No direct modification of global state
  • Mocks target only external dependencies (database, API, file system)
  • No hardcoded paths or environment dependencies
  • Exception scenarios have corresponding tests
  • Boundary values (empty list, zero, None, etc.) have corresponding tests
  • All debug print statements have been removed
  • ruff and basedpyright checks report no errors