2. unittest框架入门:unittest核心组件与第一个测试用例
好,咱们开始进入正题。unittest 是 Python 自带的测试框架,说白了就是官方给你准备好的测试工具箱。我刚开始写后端时,总觉得测试是测试工程师的事,直到有一次我改了一个工具函数,结果把整个订单模块搞崩了……嗯,从那以后,我再也不敢不写单元测试了。
2.1 unittest 的核心组件
unittest 有四个核心组件,我习惯把它们记成「四件套」:TestCase、TestSuite、TestRunner、TestLoader。咱们一个一个来看。
| 组件 | 作用 | 我个人的理解 |
|---|---|---|
| TestCase | 测试用例的基类,每个测试用例都继承它 | 就像一张试卷,每个题目是一个测试方法 |
| TestSuite | 测试用例的集合,可以组合多个用例 | 把几张试卷装订成一本练习册 |
| TestRunner | 执行测试用例并输出结果 | 就是那个批改试卷的老师 |
| TestLoader | 自动发现并加载测试用例 | 帮你去书包里翻出所有试卷的人 |
核心要点:TestCase 是基础,其他三个都是围绕它服务的。你写测试,本质上就是在写 TestCase 的子类。
2.2 编写第一个测试用例
咱们直接上手。假设你有一个计算器模块 calculator.py:
# calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("除数不能为0")
return a / b
现在来写测试。我个人习惯把测试文件和源码放在同一级目录下,文件名加个 test_ 前缀:
# test_calculator.py
import unittest
from calculator import add, divide
class TestCalculator(unittest.TestCase):
def test_add(self):
self.assertEqual(add(1, 2), 3)
self.assertEqual(add(-1, 1), 0)
self.assertEqual(add(0, 0), 0)
def test_divide(self):
self.assertEqual(divide(10, 2), 5)
self.assertEqual(divide(5, 2), 2.5)
# 测试异常情况
with self.assertRaises(ValueError):
divide(10, 0)
if __name__ == '__main__':
unittest.main()
小技巧:测试方法名必须以 test_ 开头,否则 unittest 不会执行它。我曾经因为拼写成了 testAdd(没有下划线),排查了半天……
运行方式很简单:
python test_calculator.py
你会看到类似这样的输出:
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
两个点表示两个测试都通过了。如果失败会显示 F,错误会显示 E。
2.3 断言方法详解
断言是测试的灵魂。说白了,断言就是在说「我赌这里的结果应该是这样」。unittest 提供了丰富的断言方法,我挑最常用的给你列出来:
| 断言方法 | 检查内容 | 使用场景 |
|---|---|---|
| assertEqual(a, b) | a == b | 最常用,检查值相等 |
| assertNotEqual(a, b) | a != b | 确保两个值不同 |
| assertTrue(x) | bool(x) is True | 检查条件为真 |
| assertFalse(x) | bool(x) is False | 检查条件为假 |
| assertIs(a, b) | a is b | 检查对象身份(内存地址) |
| assertIsNone(x) | x is None | 检查返回值是否为 None |
| assertIn(a, b) | a in b | 检查元素是否在容器中 |
| assertRaises(Exc, fun) | 调用 fun 时抛出 Exc 异常 | 测试异常处理逻辑 |
注意:assertEqual 和 assertIs 的区别很大。assertEqual 比较值,assertIs 比较内存地址。对于列表、字典这种可变对象,两个内容相同的列表用 assertEqual 会通过,但 assertIs 不会。我踩过这个坑——测试一个缓存函数时,用 assertIs 检查返回值,结果明明内容一样却一直报错。
来看一个综合示例:
class TestAssertions(unittest.TestCase):
def test_assertions_demo(self):
# 值相等
self.assertEqual(2 + 2, 4)
# 布尔值
self.assertTrue(1 < 2)
self.assertFalse(1 > 2)
# 对象身份
a = [1, 2, 3]
b = a
self.assertIs(a, b) # 通过,因为指向同一个对象
# 成员关系
self.assertIn('a', 'hello world')
# 异常
with self.assertRaises(ZeroDivisionError):
1 / 0
# 浮点数比较(重要!)
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
避坑指南:浮点数比较一定要用 assertAlmostEqual!0.1 + 0.2 在计算机里不等于 0.3,这是 IEEE 754 标准决定的。我曾经在测试一个金融计算模块时,直接用 assertEqual 比较金额,结果测试一直失败,后来才发现是浮点数精度问题。
2.4 TestSuite 和 TestRunner 的使用
当测试用例多了以后,你可能想按模块或功能分组执行。这时候 TestSuite 就派上用场了。
import unittest
from test_calculator import TestCalculator
from test_user import TestUser
# 创建测试套件
suite = unittest.TestSuite()
# 添加单个测试方法
suite.addTest(TestCalculator('test_add'))
suite.addTest(TestCalculator('test_divide'))
# 添加整个测试类
suite.addTest(unittest.makeSuite(TestUser))
# 创建运行器并执行
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
verbosity=2 表示详细输出,会显示每个测试方法的名称。我个人习惯在调试时用 verbosity=2,正式跑 CI 时用默认值。
2.5 TestLoader 自动发现测试
手动添加测试用例太累了。你想想看,一个项目几十个测试文件,一个个 addTest 得加到什么时候?TestLoader 就是来解决这个问题的。
import unittest
# 自动发现当前目录下所有 test_*.py 文件中的测试用例
loader = unittest.TestLoader()
suite = loader.discover('.', pattern='test_*.py')
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
discover 方法会递归查找子目录。默认情况下,它会找所有匹配 test*.py 的文件。我建议把测试文件统一放在 tests/ 目录下,然后用 discover 一次性加载。
我的习惯:在项目根目录放一个 run_tests.py,里面就几行代码调用 discover。这样不管是本地开发还是 CI 环境,执行测试都只需要一条命令:python run_tests.py。
2.6 小结
这一章我们走通了 unittest 的完整流程:从 TestCase 写测试用例,到断言方法验证结果,再到 TestSuite 组合用例,最后用 TestRunner 执行。下一章我们会深入 fixtures(setUp/tearDown),看看怎么优雅地管理测试前后的准备工作。
嗯,记住一句话:测试不是负担,是你代码的保险单。写测试花的时间,会在调试时十倍百倍地还给你。