← Back to 文字

pytest 与最小测试

本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。

学习目标

学完本节后,你应当能够:

  • 使用 pytest 为月份 1 的核心逻辑写测试。
  • 覆盖正常路径和异常路径。
  • 理解为什么 AI 项目也需要传统测试。

前置知识

  • 已完成工程化前 3 节

1. 为什么 AI 项目仍然要写测试

月份 1 中很多逻辑其实不依赖模型随机性,可以稳定测试:

  • 配置读取
  • 数据模型校验
  • 工具执行器
  • 请求参数转换
  • 错误处理

如果这些基础逻辑不稳定,后面你会把“模型问题”和“程序问题”混在一起。

2. 第一个测试

假设你有如下函数:

def normalize_text(text: str) -> str:
    return text.strip()

测试文件 tests/test_utils.py

from app.utils import normalize_text


def test_normalize_text_should_strip_whitespace() -> None:
    assert normalize_text(" hello ") == "hello"

运行:

uv run pytest -q

3. 异常路径测试

import pytest

from app.config import ensure_api_key


def test_ensure_api_key_should_raise_when_missing() -> None:
    with pytest.raises(ValueError):
        ensure_api_key(None)

这类测试非常重要,因为月份 1 后续大量逻辑都涉及失败路径。

4. 本月优先测试什么

必测

  • 配置加载
  • Pydantic 模型校验
  • 工具执行器
  • 纯函数工具

可后补

  • 真实模型调用
  • 复杂流式链路

原因:

  • 真实模型调用成本高、稳定性弱
  • 月份 1 先把确定性逻辑测起来

5. 推荐测试目录

tests/
├── test_config.py
├── test_models.py
└── test_utils.py

6. 写测试时的规则

  • 测试名要表达行为
  • 一个测试只验证一个重点
  • 优先小而稳定
  • 正常路径和异常路径都要覆盖

7. 实操任务

  1. ChatRequest 写一个正常实例化测试。
  2. ChatRequest 写一个非法 temperature 的异常测试。
  3. 给某个工具函数写一个正常路径测试。

8. 自测题

  1. 为什么月份 1 不应把所有精力都花在“测真实模型返回值”上?
  2. 为什么异常路径测试很重要?
  3. 什么样的测试算“最小但有价值”?

9. 作业与验收

验收标准:

  • 项目里至少有 3 个测试
  • 同时覆盖正常路径和异常路径
  • uv run pytest -q 能通过

10. 常见错误

  • 测试文件名不规范
  • 把多个断言堆进一个测试导致定位困难
  • 把依赖外部网络的逻辑当成最先要写的测试

11. 本章与前文关系

前面的工程化章节让你已经具备:

  • 项目骨架
  • 配置系统
  • 日志基础
  • 结构化模型

现在需要补上最后一块底层能力:验证。也就是当你写完这些结构后,如何确认它们在改动后仍然可靠。

12. 本章在研发助手项目中的位置

研发助手项目到了第 4 周时,你至少会希望有测试覆盖这些稳定逻辑:

  • 配置读取
  • 数据模型约束
  • 工具执行器
  • 核心 service 返回结构
  • /health 之类的最小接口行为

测试不是为了形式主义,而是为了让你能放心继续迭代项目。

13. 为什么 AI 项目仍然需要传统测试

很多人误以为:

模型输出本来就有不确定性,所以 AI 项目没法好好测。

这只对一部分逻辑成立。月份 1 中大量内容其实非常适合测试:

  • 结构化模型是否合法
  • 配置缺失时是否报错
  • 工具执行器是否能找到工具
  • 路由层是否返回正确状态码

也就是说,AI 项目中的“不确定性”主要在模型生成本身,而不是整个项目的全部逻辑。

14. 确定性逻辑和非确定性逻辑如何区分

确定性逻辑

相同输入,应该稳定得到相同输出。

例如:

  • normalize_text(" hello ") -> "hello"
  • 缺少 API Key 应抛出 ValueError
  • temperature=3 应被模型校验拦住

非确定性逻辑

相同输入,模型可能出现不同自然语言输出。

例如:

  • 自由文本总结
  • 创意生成

月份 1 的测试策略是:

  • 先测确定性逻辑
  • 对非确定性逻辑尽量测“结构和边界”,而不是执着于文本逐字相等

15. 错误示例 vs 正确示例

错误示例:把所有行为塞进一个巨型测试

def test_everything() -> None:
    ...

问题:

  • 失败时难定位
  • 可读性差
  • 很难说明这个测试真正保障了什么

正确示例:一个测试只验证一个重点

def test_chat_request_should_reject_invalid_temperature() -> None:
    ...

这种命名方式能直接表达行为和预期。

16. 完整文件级示例:tests/test_models.py

"""测试月份 1 的统一数据模型。"""

import pytest
from pydantic import ValidationError

from app.models import ChatMessage, ChatRequest


def test_chat_message_should_create_successfully() -> None:
    """当 role 和 content 合法时,模型应能正确创建。"""

    message = ChatMessage(role="user", content="请总结这个 diff")

    assert message.role == "user"
    assert message.content == "请总结这个 diff"


def test_chat_request_should_use_default_values() -> None:
    """当未显式传入 model 和 temperature 时,应使用默认值。"""

    request = ChatRequest(
        messages=[ChatMessage(role="user", content="hello")]
    )

    assert request.model == "deepseek-chat"
    assert request.temperature == 0.2


def test_chat_request_should_reject_invalid_temperature() -> None:
    """当 temperature 超出范围时,应抛出校验错误。"""

    with pytest.raises(ValidationError):
        ChatRequest(
            messages=[ChatMessage(role="user", content="hello")],
            temperature=3.0,
        )

17. 逐段解释这份完整示例

为什么测试名写得这么长

因为测试代码本质上也是文档。一个好的测试名,能让你几年后回来仍然知道它在验证什么。

为什么这里不用真实模型调用

因为这里的目标是验证数据模型,不是验证模型 API 服务质量。你应该尽量让测试聚焦。

为什么要同时测默认值和异常路径

因为月份 1 的很多 bug,恰恰出在“看似不会出问题的默认情况”与“边界值输入”。

18. 一个更接近工程逻辑的增强版测试示例

你还可以给纯函数工具写测试,例如:

from app.utils import normalize_text


def test_normalize_text_should_strip_whitespace() -> None:
    assert normalize_text(" hello ") == "hello"

这类测试非常适合月份 1,因为它:

  • 能建立信心

19. 为什么月份 1 的测试重点不在模型输出文本

因为如果你一开始就把测试建立在真实模型的自由文本上,会碰到:

  • 成本问题
  • 稳定性问题
  • 网络依赖问题

月份 1 正确的优先顺序是:

  1. 先测本地确定性逻辑
  2. 再测接口结构
  3. 最后才考虑少量集成级真实调用验证

20. 调试与排错:本章最常见问题

问题一:pytest 找不到测试

通常是命名不规范,或者当前目录不对。

问题二:测试导入失败

通常是项目结构或模块路径还没理顺,这往往暴露的是前面工程化基础没打稳。

问题三:测试写得过于依赖实现细节

好的测试更应验证行为,而不是把某个内部实现写死。

21. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能为月份 1 的核心确定性逻辑写最小测试。
  2. 能区分应该测什么,不应该优先测什么。
  3. 能写出正常路径和异常路径测试。
  4. 能用测试帮助自己后续重构。

22. 如果你卡在这里,先回看哪几章

23. 从本章过渡到下一章的桥接说明

接下来进入 LLM API 与 Prompt 模块。

到这里,你已经完成月份 1 最重要的底层准备:会 Python、会组织项目、会管理配置、会建模、会写最小测试。现在终于可以把这些基础能力接到真正的模型调用上,而且不会一上来就被项目混乱拖垮。

Fin