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),看看怎么优雅地管理测试前后的准备工作。

嗯,记住一句话:测试不是负担,是你代码的保险单。写测试花的时间,会在调试时十倍百倍地还给你。