本文要点
测试代码放在单独的文件里
每个测试的核心是调用断言方法
文件末尾那段 if __name__ == '__main__': unittest.main() 是测试入口

本文目录
先明确要测什么:一个自建的纯函数
假设你在练习目录里写了一个小工具,里面有一个负责计算的小函数,你希望改动它之后能快速确认结果没变。这就是单元测试要解决的问题。
按 unittest 文档的说明,测试用例(test case)是测试的独立单元,它检查一组特定输入是否得到某个特定响应;unittest 提供 TestCase 基类,用来创建新的测试用例。
本文只处理一个自建场景:一个只做加法、不读写文件、不访问网络的纯函数,给它写一个测试。所有文件名和内容都是练习用的自建示例,不是对真实项目的记录。动手前建议在练习目录里新建文件,例如 exercise/calc.py 和 exercise/test_calc.py,不要拿正在用的工具直接改。
先写被测试的函数。它接收两个数,返回它们的和:
# exercise/calc.py
def add(a, b):
return a + b这个函数没有副作用,输入确定,输出就确定,适合作为第一个测试对象。
测试文件怎么写:继承 TestCase,方法名以 test 开头
测试代码放在单独的文件里。按文档的说明,把测试代码放在独立模块有几个好处:测试模块可以单独从命令行运行,测试代码更容易与发布代码分开,也不容易被顺手改掉去迁就被测代码。
测试用例通过继承 unittest.TestCase 来创建。每个具体测试写成方法,方法名必须以 test 开头——文档明确说,这个命名约定告诉测试运行器哪些方法代表测试。
自建示例:
# exercise/test_calc.py
import unittest
from calc import add
class TestAdd(unittest.TestCase):
def test_add_positive(self):
self.assertEqual(add(1, 2), 3)
def test_add_zero(self):
self.assertEqual(add(0, 5), 5)
if __name__ == '__main__':
unittest.main()这里有两个测试方法,名字都以 test 开头。如果写成 check_add 或 add_test,运行器不会把它当成测试,这一点是初学者最容易踩的坑。
assertEqual 检查什么:相等就通过,不等就报失败
每个测试的核心是调用断言方法。文档把 assertEqual(first, second, msg=None) 描述为:检查 first 和 second 是否相等,如果两个值不相等,测试失败。
所以 self.assertEqual(add(1, 2), 3) 这行的读法是:把 add(1, 2) 的实际返回值放在第一个位置,把你预期的结果 3 放在第二个位置。两者相等,这条测试通过;不相等,这条测试失败。
文档还说明,当 first 和 second 类型完全相同、且属于 list、tuple、dict、set、frozenset、str 等类型时,会调用针对该类型的相等比较函数,生成更有用的默认错误信息。对数字比较来说,你得到的就是一条指出两个值不等的失败信息。
为什么用 assertEqual 而不是直接写 assert:文档解释,使用这些断言方法而不是 assert 语句,是为了让测试运行器能汇总所有测试结果并生成报告。用 assert 语句,第一个失败就可能中断后续检查。
assertEqual 还接受第三个参数 msg,用来在失败时给出自定义说明。文档说明所有断言方法都接受 msg 参数,指定后会在失败时作为错误信息使用。例如可以写成 self.assertEqual(add(1, 2), 3, '1 加 2 应为 3')。
测试入口:unittest.main() 做了什么
文件末尾那段 if __name__ == '__main__': unittest.main() 是测试入口。文档说明,unittest.main() 为测试脚本提供一个命令行接口。
它的作用是:当这个文件被直接运行时,收集本模块里的测试用例并执行。文档在讲测试套件时也提到,多数情况下调用 unittest.main() 就会做正确的事——自动收集模块里的测试用例并执行。
运行方式有两种,都来自文档的命令行接口说明:
1. 直接运行测试文件:
python exercise/test_calc.py2. 用 unittest 模块从命令行运行:
python -m unittest exercise/test_calc.py文档说明,测试模块也可以用文件路径指定,路径会被转换成模块名(去掉 .py,把路径分隔符换成点)。前提是这个文件仍然可以作为模块导入。
加 -v 参数会输出更详细的结果,文档给出的示例是 python -m unittest -v test_module。
需要说明的是:本文不展示任何实际运行输出,因为输出内容取决于你机器上的 Python 版本和实际代码。文档中给出的输出样式只是示例,不是本文的运行记录。
测试方法名决定执行顺序,别依赖先后关系
写多个测试方法时,有一个容易忽略的规则:文档说明,各个测试的运行顺序由测试方法名按字符串的内置排序决定。
这意味着 test_add_positive 和 test_add_zero 谁先跑,取决于名字的字母顺序,而不是你写在文件里的先后位置。所以每个测试方法都应该自包含,不依赖另一个测试先跑过。文档在讲测试用例时也强调,TestCase 实例的测试代码应当完全自包含,可以单独运行,也可以和任意数量的其他测试用例组合运行。
对本文这个加法函数来说,两个测试方法互不依赖,所以顺序不影响结果。但如果你以后写的测试需要先准备数据,就要用 setUp()。文档说明 setUp() 会在每个测试方法调用前立即执行,用来准备测试夹具(test fixture);如果 setUp() 抛出异常,框架会认为该测试发生错误,测试方法不会被执行。
本文的纯函数不需要准备数据,所以省略 setUp(),等被测对象开始依赖外部状态时再引入。
测试失败时先看哪几处:命名、导入和预期值
如果运行后没有得到预期结果,可以按下面几处依次检查。以下排查顺序是编辑整理的,不是对某个具体报错的记录。
1. 方法名有没有以 test 开头。不以 test 开头的方法不会被当作测试,运行器会直接跳过它。
2. 测试文件能不能导入被测模块。上面示例用的是 from calc import add,这要求 calc.py 在测试运行时能被找到。文档在讲测试发现时提醒,测试文件必须是可从项目顶层目录导入的模块或包。如果导入路径不对,会先报导入错误,而不是断言失败。
3. 预期值写在哪一侧。assertEqual 的第一个参数是实际值,第二个是预期值。写反了不会改变通过与否,但失败信息读起来会别扭。
4. 是不是把断言写成了 assert 语句。用 assert 语句时,测试运行器无法汇总结果,文档建议改用断言方法。
5. 有没有在测试方法里返回值。文档说明,从测试方法返回非 None 值的行为在 3.11 版本起已被弃用,测试方法应当只做检查,不返回结果。
这套做法的边界:只覆盖纯函数,不替代其他检查
本文演示的测试只针对一个自建的纯函数,适用范围有限。
第一,它只检查你写下的那几组输入输出。文档把测试用例描述为检查一组特定输入是否得到特定响应,所以它证明的是这几组输入下的行为,不是函数在所有输入下都正确。
第二,本文没有涉及 setUp()、tearDown()、测试套件、跳过测试、子测试等 unittest 的其他功能。文档说明这些功能存在,但本文的纯函数场景用不到,所以不展开。
第三,本文不涉及测试发现(test discovery)。文档说明,不带参数运行 python -m unittest 会启动测试发现,它要求测试文件是可从项目顶层目录导入的模块或包。本文用的是直接指定文件路径的方式,避开了这个前提。
第四,本文所有文件都是自建练习示例,路径写作 exercise/calc.py 和 exercise/test_calc.py。实际操作时请放在单独的练习目录,不要拿正在使用的工具直接改。
参考资料为 Python 官方 unittest 文档,其中关于 TestCase、assertEqual、unittest.main()、测试方法命名约定和运行顺序的说明,是本文事实部分的依据。
观看知识要点视频 18 秒
18秒无旁白要点导览,文字来自已审核正文;完整步骤、条件及参考资料见下方。
参考资料
以下原始资料提供本文依据;请结合日期、适用条件和产品说明查阅。
请结合原始资料的适用条件阅读。
返回效率工具 →