测试与工程化
High Contrast
Dark Mode
Light Mode
Sepia
Forest
1 min read78 words

测试与工程化

写测试不是浪费时间——它是你重构的底气、上线的信心。

工程化全景

graph LR CODE[代码] --> TYPE[类型检查 mypy] CODE --> LINT[代码规范 ruff] CODE --> TEST[测试 pytest] CODE --> FMT[格式化 black] TEST --> UNIT[单元测试] TEST --> INTEG[集成测试] TEST --> E2E[端到端测试] TYPE --> CI[CI/CD 管道] LINT --> CI TEST --> CI FMT --> CI style CODE fill:#e3f2fd,stroke:#1565c0,stroke-width:2px style CI fill:#c8e6c9,stroke:#388e3c,stroke-width:2px

pytest 基础

"""
pytest 测试框架
pip install pytest pytest-cov
"""
# === 被测试代码 (calculator.py) ===
class Calculator:
"""简单计算器"""
def add(self, a: float, b: float) -> float:
return a + b
def divide(self, a: float, b: float) -> float:
if b == 0:
raise ValueError("除数不能为零")
return a / b
def is_positive(self, n: float) -> bool:
return n > 0
# === 测试代码 (test_calculator.py) ===
import pytest
# 基本测试
def test_add():
calc = Calculator()
assert calc.add(2, 3) == 5
assert calc.add(-1, 1) == 0
assert calc.add(0.1, 0.2) == pytest.approx(0.3)
# 异常测试
def test_divide_by_zero():
calc = Calculator()
with pytest.raises(ValueError, match="除数不能为零"):
calc.divide(10, 0)
# 参数化测试
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
(-5, -3, -8),
])
def test_add_parametrize(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expected
# Fixture(测试夹具)
@pytest.fixture
def calc():
"""每个测试都会获得一个新的 Calculator"""
return Calculator()
def test_with_fixture(calc):
assert calc.add(1, 2) == 3
assert calc.divide(10, 2) == 5
# 运行: pytest test_calculator.py -v
# 覆盖率: pytest --cov=. --cov-report=html
print("测试框架示例准备就绪")
print("运行: pytest -v")

类型注解

"""
Python 类型注解系统
"""
from typing import TypeVar, Generic, Protocol
from collections.abc import Callable, Iterator
# === 基础类型注解 ===
def greet(name: str, times: int = 1) -> str:
return f"Hello {name}! " * times
# 容器类型(Python 3.9+)
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 95, "Bob": 87}
coords: tuple[float, float] = (3.14, 2.72)
unique: set[int] = {1, 2, 3}
# 可选类型
def find_user(user_id: int) -> dict | None:  # Python 3.10+
return None
# 联合类型
def process(data: str | bytes) -> str:
if isinstance(data, bytes):
return data.decode()
return data
# === 泛型 ===
T = TypeVar("T")
class Stack(Generic[T]):
"""泛型栈"""
def __init__(self):
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
# 使用
int_stack: Stack[int] = Stack()
int_stack.push(42)
str_stack: Stack[str] = Stack()
str_stack.push("hello")
# === Protocol(结构化子类型)===
class Drawable(Protocol):
"""可绘制协议"""
def draw(self) -> str: ...
class Circle:
def draw(self) -> str:
return "○"
class Square:
def draw(self) -> str:
return "□"
def render(shape: Drawable) -> None:
"""接受任何有 draw 方法的对象"""
print(shape.draw())
render(Circle())  # 不需要继承,有 draw 就行
render(Square())
# === 回调类型 ===
def apply_operation(
x: int,
y: int,
operation: Callable[[int, int], int],
) -> int:
return operation(x, y)
result = apply_operation(5, 3, lambda a, b: a + b)
print(f"结果: {result}")
# 类型检查: mypy your_file.py
print("运行类型检查: mypy .")

代码质量工具链

"""
代码质量工具链
"""
TOOLS = {
"ruff": {
"用途": "超快的代码检查和格式化(替代 flake8 + isort + black)",
"安装": "pip install ruff",
"使用": [
"ruff check .        # 检查代码问题",
"ruff check --fix .  # 自动修复",
"ruff format .       # 格式化代码",
],
"配置 (pyproject.toml)": """
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
""",
},
"mypy": {
"用途": "静态类型检查",
"安装": "pip install mypy",
"使用": ["mypy ."],
"配置 (pyproject.toml)": """
[tool.mypy]
python_version = "3.12"
strict = true
""",
},
"pytest": {
"用途": "测试框架",
"安装": "pip install pytest pytest-cov",
"使用": [
"pytest                     # 运行测试",
"pytest -v                  # 详细模式",
"pytest --cov=src           # 覆盖率",
"pytest -x                  # 失败即停",
],
"配置 (pyproject.toml)": """
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --cov=src --cov-report=term-missing"
""",
},
"pre-commit": {
"用途": "提交前自动检查",
"安装": "pip install pre-commit",
"使用": [
"pre-commit install          # 安装 hooks",
"pre-commit run --all-files  # 手动运行",
],
"配置 (.pre-commit-config.yaml)": """
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
- id: ruff-format
""",
},
}
# === pyproject.toml 完整模板 ===
PYPROJECT = """
[project]
name = "my-project"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.100",
"uvicorn>=0.20",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"mypy>=1.0",
"ruff>=0.8.0",
]
[tool.ruff]
line-length = 88
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.mypy]
strict = true
"""
for tool, info in TOOLS.items():
print(f"\n🔧 {tool}: {info['用途']}")
print(f"   安装: {info['安装']}")

项目结构模板

"""
推荐的 Python 项目结构
"""
PROJECT_TEMPLATE = """
my-project/
├── pyproject.toml          # 项目配置(替代 setup.py)
├── README.md
├── .gitignore
├── .pre-commit-config.yaml
│
├── src/                    # 源代码
│   └── my_project/
│       ├── __init__.py
│       ├── main.py
│       ├── models.py
│       ├── services.py
│       └── utils.py
│
├── tests/                  # 测试
│   ├── __init__.py
│   ├── conftest.py         # 共享 fixtures
│   ├── test_main.py
│   └── test_services.py
│
├── docs/                   # 文档
│   └── README.md
│
└── scripts/                # 脚本
└── setup.sh
"""
print(PROJECT_TEMPLATE)

下一章:实战项目——数据管道 + API 服务完整实现。