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
- Determine the test type — refer to the decision tree above and choose unit testing, integration testing, or asynchronous testing
- Create the test file —
test_<module>.py - Write the test function — follow the AAA pattern
- Add necessary fixtures — reuse test data and resources
- Mock external dependencies — keep tests isolated and fast
- Verify coverage —
pytest --cov - Run the tests —
pytest -v
Diagnosing test failures
- View detailed output —
pytest -vorpytest -vv - Print debugging information — use
print()in tests (remove before committing) - Run a single test —
pytest tests/test_file.py::test_function - 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
printto 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
printstatements have been removed - ruff and basedpyright checks report no errors