11 测试、质量与调试
大约 3 分钟编程语言Python测试pytest调试
11 测试、质量与调试
好的测试固定行为边界,不复制实现。优先测试状态转换、错误路径和真实副作用边界。
1. unittest 与 pytest
标准库 unittest 不增加依赖:
import unittest
def normalize(value: str) -> str:
return value.strip().lower()
class NormalizeTests(unittest.TestCase):
def test_normalize(self) -> None:
self.assertEqual(normalize(" API "), "api")
if __name__ == "__main__":
unittest.main()pytest 通常提供更简洁的断言、fixture 和参数化:
import pytest
@pytest.mark.parametrize(
("raw", "expected"),
[
(" API ", "api"),
("db", "db"),
("", ""),
],
)
def test_normalize(raw: str, expected: str) -> None:
assert raw.strip().lower() == expected2. 测试结构
建议遵循 Arrange-Act-Assert:
def test_check_result_marks_failure() -> None:
# Arrange
result = {"healthy": False, "reason": "timeout"}
# Act
status = "failed" if not result["healthy"] else "ok"
# Assert
assert status == "failed"
assert result["reason"] == "timeout"一个测试聚焦一个行为。名称描述场景和结果,例如 test_parse_port_rejects_zero,不要使用 test_1。
3. fixture 与临时资源
from pathlib import Path
import pytest
@pytest.fixture
def config_file(tmp_path: Path) -> Path:
path = tmp_path / "config.json"
path.write_text('{"timeout": 5}', encoding="utf-8")
return path
def test_config_file(config_file: Path) -> None:
assert config_file.read_text(encoding="utf-8").startswith("{")fixture 适合构造测试上下文、临时目录、数据库会话和固定数据。不要在 fixture 中隐藏主要断言。
4. 替身与边界
优先使用小 Protocol 和显式假对象:
class FakeClock:
def __init__(self, value: float) -> None:
self.value = value
def now(self) -> float:
return self.value
def test_remaining_is_zero_after_deadline() -> None:
clock = FakeClock(20.0)
assert max(0.0, 10.0 - clock.now()) == 0.0确需修补时才使用 monkeypatch:
def test_environment_override(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("APP_MODE", "test")
assert "APP_MODE" in __import__("os").environ比“mock 所有东西”更好的顺序通常是:
- 用纯函数和依赖注入隔离核心逻辑。
- 用假对象控制外部边界。
- 用临时文件、临时数据库和本地假服务做集成测试。
- 只对不可替代的远端或故障注入使用低层 patch。
5. 异常与失败测试
import pytest
def parse_port(raw: str) -> int:
value = int(raw)
if not 1 <= value <= 65535:
raise ValueError("port out of range")
return value
def test_parse_port_rejects_out_of_range() -> None:
with pytest.raises(ValueError, match="out of range"):
parse_port("70000")只断言异常类型可能过宽。关键错误应同时检查错误码、上下文或匹配模式。
6. 覆盖率
python -m pytest --cov=src --cov-report=term-missing覆盖率用于发现未执行区域,不是质量评分。应重点看:
- 错误分支是否覆盖。
- 边界值是否覆盖。
- 外部系统失败是否覆盖。
- 并发取消、超时和重试是否覆盖。
- 测试是否只是因为执行到代码,而没有验证行为。
7. 静态质量门禁
常见门禁:
python -m ruff check .
python -m ruff format --check .
python -m mypy src
python -m pytest具体工具可替换,但 CI 应固定版本,避免升级 lint 规则后无理由阻塞所有分支。格式化工具负责机械规则,人工评审聚焦设计、风险和可读性。
8. 调试
标准库调试器:
import pdb
def calculate(values: list[int]) -> int:
total = sum(values)
pdb.set_trace()
return total // len(values)生产环境不要依赖交互式断点。更合适的方法:
- 在异常边界记录结构化上下文。
- 使用
logging.exception保存 traceback。 - 用
pytest -x --pdb在失败处进入调试器。 - 用
python -m trace、cProfile和tracemalloc做专项分析。
9. 测试设计检查清单
- 测试在无网络、无外部状态时能否稳定运行。
- 顺序是否影响结果。
- 随机数据是否有固定种子。
- 时间是否可注入。
- 失败时是否给出足够上下文。
- 是否测试了真实错误路径。
- 是否把临时文件写在项目的真实数据目录。
