Ch15 §04 测试与质量保障 · 讲解稿¶
章节定位: Ch15 质量层。教"如何让代码可靠": 单元测试 / 集成 / E2E / 覆盖率 / CI / TDD。 承接: §03 代码设计 → §04 质量保障 走向: §05 DevOps (CI/CD) 篇幅: ~3700 字 / 阅读 11 分钟 / 讲解 22-25 分钟
一 · 一句话总览¶
测试 = "如何保证代码能跑": 单元测试 (pytest) + 集成测试 + 覆盖率 + CI 自动化。AI 工程师的测试 ≠ 数据科学测试 (含模型评估 + 数据漂移)。
二 · 章节主线 (5 段)¶
第 1 段 · 测试金字塔 (8 分钟)¶
| 类型 | 速度 | 范围 | 数量 |
|---|---|---|---|
| 单元 | ms | 1 函数/类 | 大量 |
| 集成 | s | 多模块 | 中 |
| E2E | min | 全栈 | 少 |
3.1.1 单元测试¶
def add(a, b):
return a + b
def test_add():
assert add(1, 2) == 3
assert add(-1, 1) == 0
assert add(0, 0) == 0
pytest 自动发现 test_*.py。
3.1.2 集成测试¶
测多模块协同, 如: 数据库 + API。
def test_create_and_get_user(client):
user = {"name": "Alice"}
r = client.post("/users", json=user)
assert r.status_code == 201
r = client.get(f"/users/{user['name']}")
assert r.json()["name"] == "Alice"
3.1.3 E2E 测试¶
测完整流程。Playwright / Selenium。
第 2 段 · pytest 实战 (10 分钟)¶
3.2.1 基本结构¶
3.2.2 fixture¶
import pytest
@pytest.fixture
def model():
return SimpleModel()
@pytest.fixture
def data():
return torch.randn(10, 5)
def test_forward(model, data):
out = model(data)
assert out.shape == (10, 2)
3.2.3 参数化¶
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(-1, 1, 0),
(0, 0, 0),
])
def test_add(a, b, expected):
assert add(a, b) == expected
3.2.4 Mock¶
from unittest.mock import patch, MagicMock
@patch("requests.get")
def test_fetch(mock_get):
mock_get.return_value.json.return_value = {"data": "x"}
result = fetch_data()
assert result == "x"
替换外部依赖。
3.2.5 跳过与标记¶
@pytest.mark.skip(reason="not ready")
def test_xxx(): ...
@pytest.mark.slow
def test_long(): ...
# 跑: pytest -m "not slow"
3.2.6 覆盖率¶
报告 htmlcov/index.html。
第 3 段 · AI 项目的特定测试 (10 分钟)¶
3.3.1 模型评估 ≠ 单元测试¶
| 测试 | 测什么 |
|---|---|
| 单元 | 函数正确 |
| 模型评估 | 准确率 / F1 |
| 数据测试 | 分布 / 漂移 |
| 推理测试 | 延迟 / 吞吐 |
3.3.2 数据测试¶
def test_data_shape():
data = load_data()
assert data.shape[1] == 10 # 特征数
def test_no_nan():
data = load_data()
assert not data.isna().any().any()
def test_label_distribution():
data = load_data()
# 类别不能太不平衡
counts = data['label'].value_counts()
assert (counts.max() / counts.min()) < 10
3.3.3 模型评估¶
def test_model_accuracy():
model = load_model()
test_data = load_test()
acc = evaluate(model, test_data)
assert acc > 0.85 # 业务阈值
3.3.4 梯度检查¶
def test_gradient():
x = torch.randn(10, 5, requires_grad=True)
y = model(x)
loss = y.sum()
loss.backward()
# 梯度应有值
assert x.grad is not None
assert not torch.isnan(x.grad).any()
3.3.5 推理延迟测试¶
def test_inference_latency():
model = load_model()
x = torch.randn(1, 3, 224, 224)
# 预热
for _ in range(3): model(x)
# 计时
t0 = time.time()
for _ in range(100): model(x)
avg = (time.time() - t0) / 100
assert avg < 0.01 # 10ms
第 4 段 · TDD / Property-Based / Mutation (8 分钟)¶
3.4.1 TDD 三步¶
优点: 设计更好, 覆盖更高, 信心足。
3.4.2 Property-Based Testing¶
Hypothesis 自动生成输入:
from hypothesis import given, strategies as st
@given(st.lists(st.integers, min_size=1))
def test_sort_idempotent(xs):
assert sorted(sorted(xs)) == sorted(xs)
@given(st.lists(st.integers))
def test_reverse(xs):
assert list(reversed(list(reversed(xs)))) == xs
发现边界 case。
3.4.3 Mutation Testing¶
mutmut 故意改代码, 看测试能否捕获。
3.4.4 模糊测试 (Fuzzing)¶
atheris (Python):
import atheris
@atheris.instrument_func
def TestOneInput(input_bytes):
try:
parse(input_bytes)
except ValueError:
return # 期望错误
except Exception:
raise # bug
atheris.Setup([], TestOneInput)
atheris.Fuzz()
第 5 段 · CI / 代码质量 (8 分钟)¶
3.5.1 GitHub Actions 测试¶
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- run: pip install -e ".[dev]"
- run: ruff check .
- run: mypy src/
- run: pytest --cov=mypkg --cov-fail-under=80
- uses: codecov/codecov-action@v3
3.5.2 Lint 与 Format¶
| 工具 | 用途 |
|---|---|
| ruff | 快 lint (替代 flake8) |
| black | 不可配置格式化 |
| mypy | 静态类型 |
| isort | import 排序 |
| pylint | 深度 lint |
3.5.3 Pre-commit¶
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
hooks:
- id: ruff
- id: ruff-format
git commit 前自动跑。
3.5.4 测试驱动开发的 AI 项目¶
- 数据契约测试: 验证 schema, 分布
- 模型回归测试: 新模型 ≥ 旧模型
- 集成测试: 端到端 pipeline
- 性能测试: 延迟 / 吞吐不降
三 · 易错点 (5 条)¶
- 测试覆盖率 100% 陷阱: 高覆盖 ≠ 无 bug, 看测试质量。
- flaky 测试: 偶发失败, 用 fixture 隔离时间 / 随机。
- 测试慢: 网络/磁盘 IO 用 mock。
- 过度 mock: 测的是 mock, 不是代码。
- 缺测试的合并: CI 应阻断。
四 · 跨章连接¶
| 章节 | 关联 |
|---|---|
| Ch13 §05 编程语言 | 类型提示 → mypy |
| Ch14 §04 算法 | 算法测试 |
| Ch15 §05 DevOps | CI/CD |
五 · 自测 5 题¶
- 测试金字塔三层?
- pytest fixture 用途?
- mock 什么时候用?
- mutation testing 用途?
- 覆盖率 100% 等价于无 bug 吗?
六 · 一句话总结¶
测试 = 质量保障。单元 (pytest) + 集成 + AI 特有 (数据/模型/延迟) + CI 自动化。AI 项目测试 ≠ 数据科学测试 (含数据 + 模型 + 服务)。Ch15 §05 进入 DevOps。
七 · 板书建议¶
[板书 1: 金字塔]
单元 / 集成 / E2E
大量 / 中 / 少
[板书 2: pytest]
fixture / parametrize
mock
[板书 3: AI 特有]
数据 + 模型 + 推理延迟
[板书 4: CI]
lint + test + 覆盖率门禁
八 · 参考资料¶
- pytest 官方文档
- "Test Driven Development" (Kent Beck)
- Hypothesis 文档
- GitHub Actions 文档
- Ch14 §04 测试 (本套书)