02.12-Agent学习记录-单元测试与集成测试

会议通知程序能够运行以后,我们给它加了一项要求:标题前后的空格应当去掉;如果标题只有空格,就明确报错。修改看起来不大,但怎样确认它既处理了新情况,又没有改坏正常通知?

舒翱移动端 / 全栈 / Agent 应用

正文

每次手工打开程序,输入几个例子,也能发现部分问题。不过,随着查询、改写与保存功能增加,靠记忆重复检查会越来越困难。测试可以把这些预期记录下来,让程序帮助我们反复比较实际行为。

测试的基本过程,是准备条件和输入,执行被检查的部分,再判断结果是否符合预期。 预期需要来自需求或接口约定,不能仅仅因为当前代码这样输出,就认定它应该如此。

先看一个小范围的行为。假设 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 异常处理与资源清理。这些支系会在各自编号中单独展开。

系列阅读

本篇学习编号:02.12 单元测试与集成测试。

上一篇: 02.11 依赖管理与环境隔离 。

后续还有:03.01 向量、矩阵与张量,将在本系列后续发布。

延伸阅读

返回全部文章

搜索项目和文章

输入关键词,查找项目或文章。

使用 Tab 选择结果,按 Esc 关闭。