0%

Python单元测试

Python单元测试:告别“手动点点点”的自动化之道

在Python开发的世界里,我们常常陷入一种循环:写代码 -> 运行 -> 发现Bug -> 修Bug -> 运行 -> 发现新Bug。如果你的测试方式还停留在写几个 print() 或者在浏览器里手动点击按钮,那么当项目代码量超过几千行时,这种“手工测试”将成为你最大的噩梦。

单元测试,就是拯救你于水火之中的“自动化疫苗”。我们来聊聊如何用 Python 的测试工具,构建坚不可摧的代码防线。


为什么你需要单元测试?

想象一下,你正在开发一个电商系统。你刚修复了“购物车结算”的一个小错误,结果上线后发现“用户登录”功能挂了。这就是典型的回归错误

单元测试的核心价值在于:

  • 快速反馈:代码写完即测,不用等到上线。
  • 重构的安全网:想优化代码结构?只要测试全过,你就知道没把核心逻辑改坏。
  • 活文档:测试用例就是最好的API说明书,告诉你这个函数该怎么用。

两大门派:unittest 与 pytest

Python 的测试生态主要由两位“大佬”把持:内置的 unittest 和第三方霸主 pytest

unittest:学院派的老大哥

它是 Python 的标准库,无需安装。它的风格比较“Java风”,需要写类、继承 unittest.TestCase,代码结构严谨但略显繁琐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 传统的 unittest 风格
import unittest

class TestMath(unittest.TestCase):
# unittest会自动查找 test_ 前缀的方法,然后进行执行
def test_add(self):
self.assertEqual(1 + 1, 2) # 必须用特定的断言方法
# 初始化数据或清理资源。unittest 提供了 setUp() 和 tearDown() 方法
def setUp(self):
# 在每个 test_ 方法执行前运行
print("连接数据库...")
self.connection = "mock_db_connection"

def tearDown(self):
# 在每个 test_ 方法执行后运行
print("关闭数据库连接...")
self.connection = None

if __name__ == '__main__':
unittest.main()

运行时有两种方式

  • 直接运行脚本

    1
    python TestMath.py
  • 使用unittest模块的命令行接口

    1
    python -m unittest TestMath.py

pytest:现代派的极简主义者

这是目前业界的主流选择。它语法简洁,支持直接写函数,自动发现测试文件,且拥有极其丰富的插件生态。

1
2
3
# 现代的 pytest 风格
def test_add():
assert 1 + 1 == 2 # 直接使用原生断言,简洁明了

建议:新项目直接用 pytest,老项目维护可能会遇到 unittest


实战:用 pytest 构建测试体系

让我们通过一个简单的“计算器”案例,掌握测试的核心流程。

第一步:编写业务代码

假设我们有一个 calculator.py

1
2
3
4
def divide(a, b):
if b == 0:
raise ValueError("除数不能为零!")
return a / b

第二步:编写测试用例

创建 test_calculator.py。注意,测试文件通常以 test_ 开头。

1
2
3
4
5
6
7
8
9
10
11
12
import pytest
from calculator import divide

# 1. 测试正常情况
def test_divide_success():
assert divide(10, 2) == 5

# 2. 测试异常情况(除零)
def test_divide_by_zero():
# 验证代码是否抛出了预期的异常
with pytest.raises(ValueError):
divide(10, 0)

第三步:运行测试

在终端输入 pytest,你会看到清晰的报告:

1
2
test_calculator.py::test_divide_success PASSED
test_calculator.py::test_divide_by_zero PASSED

进阶技巧:参数化与 Mock

当你要测试几十种输入组合,或者代码依赖外部API时,简单的断言就不够用了。

参数化:拒绝重复代码

如果你想测试 1+1=2, 2+2=4, 3+3=6,不用写三个函数,用 @pytest.mark.parametrize 即可:

1
2
3
4
5
6
7
8
9
import pytest

@pytest.mark.parametrize("a, b, expected", [
(1, 1, 2),
(2, 2, 4),
(3, 3, 6)
])
def test_add(a, b, expected):
assert a + b == expected

Mock:隔离外部依赖

如果你的代码需要调用微信API或数据库,测试时肯定不想真的发一条微信或修改数据库。这时需要用 Mock(模拟对象)来“伪造”外部服务。

1
2
3
4
5
6
7
8
9
10
11
12
# 假设 get_user_info 调用了真实的网络请求
def test_get_user_info(mocker):
# 伪造一个返回值为 {'name': 'Alice'} 的响应
mock_response = mocker.Mock()
mock_response.json.return_value = {'name': 'Alice'}

# 替换掉真实的请求函数
mocker.patch("requests.get", return_value=mock_response)

# 此时运行测试,不会真的联网
result = get_user_info(123)
assert result['name'] == 'Alice'

避坑指南:测试的“三不原则”

很多新手写了测试,但写成了“负资产”。请遵守以下原则:

不要测试“实现细节”

  • 错误做法:测试某个私有变量是不是等于5。
  • 正确做法:测试函数的输入输出是否符合预期。只要结果对,内部怎么变都行。

不要有测试依赖

  • 错误做法:测试B必须在测试A运行成功后才能跑。
  • 正确做法:每个测试都是独立的。测试顺序不应该影响结果。

不要忽视边界条件

  • 不要只测正常数据。空列表、负数、超长字符串、None 值,这些才是Bug的藏身之处。

总结:从“点点点”到自动化

单元测试不仅仅是找Bug,更是一种设计思维。当你发现一个函数很难写测试时,通常意味着这个函数功能太杂、耦合太重,需要重构了。

最佳实践工作流

  1. 写一个失败的测试(红)。
  2. 写最少的代码让测试通过(绿)。
  3. 优化代码结构(重构)。

欢迎关注我的其它发布渠道