Python单元测试案例如何编写测试用例(附实战代码)
目录导读
- 为什么要写单元测试?——三个不得不面对的现实
- 单元测试基础:unittest与pytest的取舍
- 案例一:测试一个数学工具函数(入门级)
- 案例二:测试一个带外部依赖的API调用(中级)
- 案例三:测试一个数据库操作的真实场景(高级)
- 常见问题与避坑指南(Q&A)
- 好的测试用例是项目的一半质量保证
为什么要写单元测试?——三个不得不面对的现实
很多Python开发者都会问:“我的代码能跑,为什么还要花时间写测试?”

真实案例拆解:假设你写了一个电商折扣计算函数,上线后产品经理突然说:“满100减20,但如果是VIP用户要打9折,且两种优惠不能叠加。”如果没有任何测试,你改完代码,很可能就忘记了检查边界情况——刚好100元且是VIP”时,折扣到底怎么算?错误一旦上线,可能直接导致公司损失真实订单。
三大核心价值:
- 早期发现bug:修改代码后,一条
pytest命令就能告诉你哪里出了问题,而不是等QA手动点100次页面。 - 重构的安全网:没有测试的代码重构等于徒手拆炸弹;有测试,你可以大胆调整内部逻辑。
- 文档即代码:测试用例清晰地说明了“这个函数在什么输入下应该返回什么结果”,比任何注释都准确。
单元测试基础:unittest与pytest的取舍
Python官方标准库自带unittest,但社区更推荐pytest,为什么?
| 对比维度 | unittest | pytest |
|---|---|---|
| 语法复杂度 | 需要类和方法,self.assertEqual 较为啰嗦 |
纯函数式 assert,代码量减少50% |
| 插件生态 | 较有限 | 强大(如pytest-mock、pytest-cov) |
| 参数化测试 | 需额外写subTest |
@pytest.mark.parametrize 一行搞定 |
| 执行速度 | 中等 | 更快且支持并行执行 |
本文所有案例均使用pytest,因为它更贴近现代Python开发习惯,且符合Google和Meta的内部测试标准。
案例一:测试一个数学工具函数(入门级)
假设我们有这样一个函数(calculator.py):
def calculate_discount(price: float, is_vip: bool) -> float:
if not isinstance(price, (int, float)):
raise TypeError("price must be numeric")
base_discount = 0.9 if price >= 100 else 1.0
vip_discount = 0.95 if is_vip else 1.0
return round(price * base_discount * vip_discount, 2)
如何测试它?直接看测试用例(test_calculator.py):
import pytest
from calculator import calculate_discount
class TestCalculateDiscount:
@pytest.mark.parametrize("price, vip, expected", [
(100, False, 90.0), # 满100打9折
(100, True, 85.5), # 满100+VIP:0.9*0.95=0.855
(50, False, 50.0), # 不满100,不打折
(50, True, 47.5), # 仅VIP折扣
(0, False, 0.0), # 边界:0元
("abc", False, None), # 异常输入
])
def test_discount(self, price, vip, expected):
if price == "abc":
with pytest.raises(TypeError):
calculate_discount(price, vip)
else:
assert calculate_discount(price, vip) == expected
关键点:我们用parametrize覆盖了正常值、边界值、异常值三种情况,这比手动写6个测试函数效率高得多。
案例二:测试一个带外部依赖的API调用(中级)
真实项目中,函数经常调用外部服务(如支付网关、天气API),直接测试会依赖网络,导致测试不稳定,解决方案是Mock。
目标函数(order_service.py):
import requests
def check_payment(order_id: str) -> bool:
response = requests.get(f"https://api.payment.com/v1/orders/{order_id}/status")
data = response.json()
return data.get("paid", False)
测试用例(test_order.py):
import pytest
from unittest.mock import patch
from order_service import check_payment
class TestCheckPayment:
@patch("order_service.requests.get")
def test_payment_success(self, mock_get):
# 模拟成功返回
mock_get.return_value.json.return_value = {"paid": True}
assert check_payment("ORD-123") == True
@patch("order_service.requests.get")
def test_payment_failed(self, mock_get):
mock_get.return_value.json.return_value = {"paid": False}
assert check_payment("ORD-456") == False
@patch("order_service.requests.get")
def test_network_error(self, mock_get):
mock_get.side_effect = requests.ConnectionError("网络不可达")
with pytest.raises(requests.ConnectionError):
check_payment("ORD-789")
为什么这样做?
- 不需要真实API密钥或网络。
- 测试速度从秒级降到毫秒级。
- 可以模拟各种网络错误,这在真实环境中很难触发。
案例三:测试一个数据库操作的真实场景(高级)
测试数据库函数时,最忌讳污染生产数据库,推荐使用SQLite内存数据库或pytest fixtures。
示例函数(user_repo.py):
def create_user(conn, name: str, age: int) -> int:
cursor = conn.cursor()
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", (name, age))
conn.commit()
return cursor.lastrowid
测试用例(test_user.py):
import pytest
import sqlite3
from user_repo import create_user
@pytest.fixture
def db_connection():
conn = sqlite3.connect(":memory:") # 内存数据库
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")
yield conn
conn.close()
class TestCreateUser:
def test_create_normal_user(self, db_connection):
user_id = create_user(db_connection, "Alice", 30)
assert user_id == 1 # 第一个用户ID为1
def test_duplicate_name(self, db_connection):
create_user(db_connection, "Bob", 25)
# 假设数据库有唯一约束,测试异常
with pytest.raises(sqlite3.IntegrityError):
create_user(db_connection, "Bob", 30)
def test_invalid_age_type(self, db_connection):
with pytest.raises(TypeError):
create_user(db_connection, "Charlie", "twenty")
核心技巧:
fixture每次测试前自动创建新数据库,测试后自动销毁,互不干扰。- 使用
memory:避免磁盘I/O,测试更快。
常见问题与避坑指南(Q&A)
Q1:测试覆盖率一定要100%吗?
A:不一定,建议优先覆盖核心业务逻辑和边界条件,目标是80%以上,而非100%,盲目的100%覆盖率可能导致测试只测不重要的代码行。
Q2:如何测试私有方法或内部函数?
A:Python没有真正的私有,如果函数名以开头,可以直接在测试模块中调用,但更好的做法是:只测试外部公共接口,内部逻辑会通过公共方法被间接测试到。
Q3:pytest和unittest能混用吗?
A:可以,pytest能自动发现并运行unittest.TestCase的子类,但建议统一使用pytest以避免风格混乱。
Q4:测试文件应该放在哪里?
A:推荐两种模式:
- 紧耦合:每个模块同级放
test_xxx.py(适合小项目) - 分离目录:统一放在
tests/目录下,内部结构镜像源码(适大中型项目)
Q5:如何避免测试之间的依赖?
A:坚持独立测试原则:
- 每个测试函数不依赖其他测试的执行顺序。
- 使用
fixture清理和重建数据。 - 避免全局状态(如全局变量、单例模式)。
好的测试用例是项目的一半质量保证
你不需要写“完美”的测试,只需要写“有用”的测试,从最简单的parametrize开始,逐步引入mock和fixture,最终你的测试用例会成为团队最可靠的文档和bug防护网。
小建议:先在当前项目里挑一个你改过的函数,写3个测试(正常/异常/边界),运行pytest -v看结果——你会在30秒内感受到“安全感”。
当测试变成习惯,重构将不再是噩梦,而是乐趣。