正文
每次手工打开程序,输入几个例子,也能发现部分问题。不过,随着查询、改写与保存功能增加,靠记忆重复检查会越来越困难。测试可以把这些预期记录下来,让程序帮助我们反复比较实际行为。
测试的基本过程,是准备条件和输入,执行被检查的部分,再判断结果是否符合预期。 预期需要来自需求或接口约定,不能仅仅因为当前代码这样输出,就认定它应该如此。
先看一个小范围的行为。假设 notice.py 中有这样的教学代码:
from pathlib import Path
def make_notice(title):
cleaned = title.strip()
if not cleaned:
raise ValueError("会议标题不能为空")
return f"会议通知:{cleaned}"
def save_notice(title, path):
destination = Path(path)
destination.write_text(make_notice(title), encoding="utf-8")
return destination
make_notice 只接收本例约定的字符串标题。strip 去掉前后空白;处理后为空,就抛出 ValueError;否则把标题放进固定通知格式。
save_notice 则把生成结果写到指定路径,并返回相应路径对象。它是一份简化实现,会写入给定文件;真实应用还要根据要求设计覆盖规则、访问范围和异常处理。本篇只用临时文件说明测试,不操作用户的会议草稿。路径对象的文本读写
如果只想检查标题处理,可以直接测试 make_notice。测试不需要联网,不需要真实日历,也不需要打开整个应用。这是理解单元测试的一个入口:把相对小而明确的行为,放在可控制的条件下检查。
可以在同目录准备 test_notice.py:
import unittest
from notice import make_notice
class NoticeTests(unittest.TestCase):
def test_normal_title(self):
self.assertEqual(
make_notice(" 项目例会 "),
"会议通知:项目例会"
)
def test_blank_title(self):
with self.assertRaises(ValueError):
make_notice(" ")
unittest 是 Python 标准库提供的测试框架。NoticeTests 继承 TestCase,里面以 test 开头的方法会被测试程序发现。
第一个方法把带前后空格的标题交给函数,用 assertEqual 比较实际结果与明确的预期字符串。第二个方法用 assertRaises 检查空白标题是否引发指定异常,而不是要求函数对所有输入都顺利返回。
这类检查叫断言。在测试中,断言不满足通常意味着测试失败;如果执行过程中发生未预期异常,则可能表现为测试错误。两种结果都需要查看原因,不能把“不通过”一概理解成同一种代码问题。
假设两个文件已按示例保存,并且处于相应目录,可以使用:
python -m unittest -v test_notice
它让当前 Python 运行 unittest,加载 test_notice 模块,-v 要求输出更详细的测试名称与结果。对于上面的实现与测试,预期这两个用例通过;这不是对真实会议应用已经完成验证的声明。unittest 的用例、断言与运行方式
运行以后,还要确认实际发现了哪些测试。模块名称写错、发现规则不匹配,可能导致目标用例根本没有执行。仅仅看到命令结束,或者看到零个用例没有失败,都不能当作预期行为已经验证。
单元的范围并没有一个只按行数或函数数量划分的统一界限。有的项目把一个函数作为单元,有的围绕一个类或一项小行为组织。重点是边界清楚,失败时容易判断检查到了什么。
接下来,我们还想知道:通知生成与文件保存配合时,能否得到预期的文件内容?
只测试 make_notice,无法发现保存时忘记指定编码、写入了错误正文等问题。这里需要让有关部分真正配合一次。在本文中,我们把格式生成函数与真实文件读写的配合,作为一个小型集成测试。
可以把下面内容接在同一测试文件中:
import tempfile
from pathlib import Path
from notice import save_notice
class SavingTests(unittest.TestCase):
def test_saved_content(self):
with tempfile.TemporaryDirectory() as directory:
target = Path(directory) / "notice.txt"
saved = save_notice("项目例会", target)
self.assertEqual(
saved.read_text(encoding="utf-8"),
"会议通知:项目例会"
)
TemporaryDirectory 提供专用临时目录。with 结束时,它按设计清理该目录及其中内容。target 指向这个目录里的 notice.txt,测试调用真实保存函数,再从文件读回内容比较。临时目录与自动清理
这里真正发生配合的,是我们的函数与本机文件系统。它没有访问日历,没有生成真实通知,也没有证明远程存储服务工作正常。集成测试覆盖的是所选组件之间的关系,不能因为名字里有“集成”,就把它理解成验证了整个产品。
不同测试之间还应尽量独立。一个用例需要的记录应当明确准备,不能依赖另一个用例碰巧先运行;测试产生的文件与连接也应妥善释放。否则,同一段程序可能因为用例顺序或上次遗留的数据而出现不同结果。
如果检查范围继续扩大,才可能涉及 HTTP 请求、日历账户或数据库。这样的测试通常需要准备可控的数据、访问条件和清理方式,避免测试之间争用同一份会议记录。
外部服务并不总适合放进每次小测试。因此,还会用到“测试替身”。
例如,查询模块可以接收一个日历客户端。测试时,用一个按约定返回固定会议记录的替代对象,让我们能够稳定制造“没有会议”“两场同名会议”或“查询失败”等情况。真实日历不必碰巧处于这些状态,测试才能进行。
Python 的 unittest.mock 提供了替代依赖、设定返回结果以及检查调用等能力。替身可以帮助控制条件,但它按照测试中的设定工作,不能据此证明真实服务确实接受同样参数或返回相同结构。测试替身与依赖替换
假设日历接口已经更换字段,我们的替身仍然返回旧格式,测试就可能全部通过,真实调用却失败。因此,替身测试与实际边界的集成检查需要互相补充,不能把模拟结果包装成真实服务验证。
测试也要包含合适的失败情况。标题为空、会议不存在、保存失败,都可能是程序需要处理的正常场景。检查它有没有清楚报错、有没有错误地继续报告成功,往往比只测试一条顺利路径更有价值。
发现失败以后,首先要判断预期是否正确,再分析实现与环境。不能为了让测试恢复通过,就直接把预期字符串改成当前输出。那样也许消除了红色提示,却可能一并删掉了本来需要守住的行为。
修复一个真实问题后,可以保留能重现它的用例,帮助后续修改发现相同问题是否重新出现。这就是回归检查的一种用途。用例应当表达重要行为,避免过分依赖无关的内部实现细节。
即使所有测试通过,结论仍然有范围。
上面的测试能够支持空格处理、空标题报错和临时文件内容等判断,却无法证明日历里的会议时间真实,也无法证明通知已发给正确的人。没有写入测试条件的规则,通常不能靠一次“全部通过”自动得到保证。
模型生成内容还可能有不同措辞。检查它是否准确引用会议时间、是否保留必要信息,与比较一段固定函数输出,往往需要不同的评价方式。模型评估与完整 Agent 评估,会在 23 模块继续展开,不能只靠这几个程序测试代替。
到这里,02 模块已经从编写程序走到了通信、数据规则、异步处理、版本、依赖与测试。后面进入数学基础,先认识向量、矩阵与张量;这些概念会帮助我们进一步理解模型内部怎样表示和计算信息。
当前学习编号:02.12 单元测试与集成测试。
下一学习编号:03.01 向量、矩阵与张量。后续还有其他学习编号。
关联分支:检查范围对应 23.01 模型评估与完整系统评估、23.02 任务成功标准与评测集、23.09 工具选择、参数与执行正确性;失败场景对应 23.18 安全、权限与失败场景评测、24.03 错误分类与根因定位。
重复验证对应 23.14 回归评测与消融实验;可控条件与资源处理对应 24.12 可复现运行与环境快照、02.09 异常处理与资源清理。这些支系会在各自编号中单独展开。