Python单元测试
Python 单元测试:别再用 print 调试了,写个测试一劳永逸
改了一行代码,然后花了半小时手动点页面验证功能对不对——这事你干过吧?
项目小的时候还能忍,项目大了就变成噩梦:改 A 测 A,改 B 测 B,改完发现 A 又被你改坏了。这种”改一处、测全部”的死循环,靠手工点永远跳不出来。
单元测试就是干这个的。把验证逻辑写成代码,每次改完跑一遍,几秒钟告诉你哪里坏了。
不写测试的代价 vs 写了测试的好处
典型场景: 你修了一个”购物车结算金额算错了”的 Bug,上线后发现”用户登录”挂了。因为修改结算逻辑的时候,不小心改到了一个公用的工具函数。
写了测试之后:
- 修 Bug → 跑测试 → 发现登录测试红了 → 知道哪里坏了 → 修好 → 再跑测试 → 全绿 → 上线
- 全程 5 分钟,不用手动点页面
单元测试的价值:
| 价值 | 说明 |
|---|---|
| 快速反馈 | 写完代码跑测试,秒级知道对不对 |
| 重构安全网 | 改完代码跑测试,全绿说明没改坏 |
| 活文档 | 测试就是最新的 API 使用示例 |
| 防止复发 | 修过的 Bug 写进测试,不会再犯 |
unittest vs pytest:用哪个?
Python 有两个主流测试框架:
| unittest(内置) | pytest(第三方) | |
|---|---|---|
| 安装 | 不用装 | pip install pytest |
| 风格 | 类 + 继承 + 特定断言 | 函数 + 原生 assert |
| 代码量 | 较冗长 | 简洁 |
| 参数化 | 麻烦 | @pytest.mark.parametrize 原生支持 |
| 生态 | 有限 | 200+ 插件 |
直接结论:新项目用 pytest,不用纠结。
unittest 是 Python 自带的,但 pytest 用起来更顺手,代码更少,社区更活跃。老项目如果已经用了 unittest 也可以继续用,但新项目建议直接用 pytest。
从零开始写第一个测试
1. 安装 pytest
pip install pytest
2. 写一个待测试的函数
# calculator.py
def divide(a, b):
if b == 0:
raise ValueError("除数不能为0")
return a / b
def add(a, b):
return a + b
3. 写测试文件
测试文件命名:test_*.py 或 *_test.py。测试函数命名:以 test_ 开头。
# test_calculator.py
import pytest
from calculator import divide, add
def test_add():
assert add(1, 2) == 3
assert add(-1, 1) == 0
def test_divide_success():
assert divide(10, 2) == 5
assert divide(9, 3) == 3
def test_divide_by_zero():
with pytest.raises(ValueError) as exc:
divide(10, 0)
assert "除数不能为0" in str(exc.value)
4. 运行测试
# 运行所有测试
pytest
# 运行指定文件
pytest test_calculator.py
# 显示详细信息(推荐)
pytest -v
# 显示 print 输出(调试时用)
pytest -s
看到 3 passed 就表示通过了。
测试结构:AAA 模式
每个测试都应该遵循 AAA 结构:
def test_something():
# Arrange(准备):准备好输入数据和环境
user_data = {"name": "Alice", "age": 25}
# Act(执行):调用被测试的函数
result = process_user(user_data)
# Assert(断言):验证结果对不对
assert result["status"] == "success"
这个模式让测试清晰、一致。每个测试只测一个行为,失败了能快速定位问题。
参数化:一组数据测同一个逻辑
如果你要测 add 函数,写好几个测试用例:
def test_add_1():
assert add(1, 1) == 2
def test_add_2():
assert add(2, 2) == 4
def test_add_3():
assert add(3, 3) == 6
太啰嗦了。用参数化:
)
def test_add(a, b, expected):
assert add(a, b) == expected
一组数据测一次,代码少、覆盖全。
Fixture:共享测试准备逻辑
如果多个测试需要同样的准备步骤,用 fixture 复用。
import pytest
def sample_user():
"""返回一个示例用户,多个测试可以共用"""
return {"name": "Alice", "age": 25, "email": "alice@example.com"}
def test_user_age(sample_user):
assert sample_user["age"] == 25
def test_user_email(sample_user):
assert "@" in sample_user["email"]
fixture 的生命周期控制:
# 整个测试会话只执行一次
def db_connection():
conn = create_connection()
yield conn
conn.close()
# 每个测试执行一次(默认)
def clean_data():
return {}
scope="session" 适合数据库连接这种”建立一次、反复使用”的资源。scope="function"(默认)适合”每次测试都需要干净数据”的场景。
Mock:隔离外部依赖
你的函数可能依赖网络请求、数据库、文件系统。单元测试要测的是”你的逻辑”,不是”外部服务能不能通”。
用 Mock 模拟外部依赖:
from unittest.mock import Mock, patch
import requests
def get_user_name(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()["name"]
def test_get_user_name():
# 模拟响应对象
mock_response = Mock()
mock_response.json.return_value = {"name": "Alice"}
# 用 mock 替换 requests.get
with patch("requests.get", return_value=mock_response):
result = get_user_name(123)
assert result == "Alice"
这样测试不用真的发网络请求,快、稳定、不依赖外部服务。
测试覆盖率
覆盖率告诉你:测试覆盖了多少代码。
# 安装
pip install pytest-cov
# 运行测试并查看覆盖率
pytest --cov=calculator test_calculator.py
# 生成 HTML 报告(可以打开看具体哪行没覆盖)
pytest --cov=calculator --cov-report=html tests/
覆盖率目标: 核心业务逻辑 90%+,一般应用 80%+。
注意:100% 覆盖率不代表没 Bug,但低覆盖率一定高风险。