跳转至

Ch15 §04 测试与质量保障 · 讲解稿

章节定位: Ch15 质量层。教"如何让代码可靠": 单元测试 / 集成 / E2E / 覆盖率 / CI / TDD。 承接: §03 代码设计 → §04 质量保障 走向: §05 DevOps (CI/CD) 篇幅: ~3700 字 / 阅读 11 分钟 / 讲解 22-25 分钟


一 · 一句话总览

测试 = "如何保证代码能跑": 单元测试 (pytest) + 集成测试 + 覆盖率 + CI 自动化。AI 工程师的测试 ≠ 数据科学测试 (含模型评估 + 数据漂移)。


二 · 章节主线 (5 段)

第 1 段 · 测试金字塔 (8 分钟)

        /\
       /  \
      / E2E \      慢, 贵, 真实
     /______\
    / 集成   \    中速, 多模块
   /__________
  / 单元测试   \   快, 隔离
 /______________\
类型 速度 范围 数量
单元 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 基本结构

# test_sample.py
def inc(x):
    return x + 1

def test_inc():
    assert inc(3) == 4

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 覆盖率

pytest --cov=mypkg --cov-report=html

报告 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 三步

Red:    写失败测试
Green:  写最少代码通过
Refactor: 重构

优点: 设计更好, 覆盖更高, 信心足。

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 故意改代码, 看测试能否捕获。

mutmut run --paths-to-mutate=mypkg/
mutmut results
mutmut show 1

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 项目

  1. 数据契约测试: 验证 schema, 分布
  2. 模型回归测试: 新模型 ≥ 旧模型
  3. 集成测试: 端到端 pipeline
  4. 性能测试: 延迟 / 吞吐不降

三 · 易错点 (5 条)

  1. 测试覆盖率 100% 陷阱: 高覆盖 ≠ 无 bug, 看测试质量。
  2. flaky 测试: 偶发失败, 用 fixture 隔离时间 / 随机。
  3. 测试慢: 网络/磁盘 IO 用 mock。
  4. 过度 mock: 测的是 mock, 不是代码。
  5. 缺测试的合并: CI 应阻断。

四 · 跨章连接

章节 关联
Ch13 §05 编程语言 类型提示 → mypy
Ch14 §04 算法 算法测试
Ch15 §05 DevOps CI/CD

五 · 自测 5 题

  1. 测试金字塔三层?
  2. pytest fixture 用途?
  3. mock 什么时候用?
  4. mutation testing 用途?
  5. 覆盖率 100% 等价于无 bug 吗?

六 · 一句话总结

测试 = 质量保障。单元 (pytest) + 集成 + AI 特有 (数据/模型/延迟) + CI 自动化。AI 项目测试 ≠ 数据科学测试 (含数据 + 模型 + 服务)。Ch15 §05 进入 DevOps。


七 · 板书建议

[板书 1: 金字塔]
   单元 / 集成 / E2E
   大量 / 中 / 少

[板书 2: pytest]
   fixture / parametrize
   mock

[板书 3: AI 特有]
   数据 + 模型 + 推理延迟

[板书 4: CI]
   lint + test + 覆盖率门禁

八 · 参考资料

  1. pytest 官方文档
  2. "Test Driven Development" (Kent Beck)
  3. Hypothesis 文档
  4. GitHub Actions 文档
  5. Ch14 §04 测试 (本套书)