python单元测试 unittest.mock.patch (二)
unittest.mock.patch
的核心能力是**“临时替换对象行为”,除了模拟异常,它在测试中还有很多高频应用场景,本质都是为了隔离外部依赖、控制测试环境、验证代码行为**。以下是最常用的几类场景,结合具体例子说明:
一、替换函数返回值(最基础用法)
当测试代码依赖某个“不好控制返回值”的函数(比如调用外部API、读取数据库、随机数生成)时,用 patch
强制让它返回固定值,避免测试结果受外部因素影响。
例子:测试依赖随机数的函数
假设我们有一个函数 is_lucky()
,它调用 random.random()
生成随机数,若大于0.5则返回“幸运”,否则返回“倒霉”。
直接测试时,结果是随机的(不稳定),用 patch
替换 random.random()
的返回值,就能稳定测试两种情况。
import random
from unittest.mock import patch# 目标函数:依赖随机数
def is_lucky():num = random.random() # 生成0-1的随机数return "幸运" if num > 0.5 else "倒霉"# 测试1:模拟random.random()返回0.6(>0.5)
with patch("random.random", return_value=0.6):assert is_lucky() == "幸运" # 必然通过# 测试2:模拟random.random()返回0.4(<0.5)
with patch("random.random", return_value=0.4):assert is_lucky() == "倒霉" # 必然通过
- 关键参数:
return_value=固定值
用于指定被替换函数的返回结果,替代原函数的逻辑。
二、验证函数是否被正确调用(调用追踪)
测试中常需要确认:某个函数是否被调用、被调用了几次、传入的参数是否正确。patch
生成的“模拟对象”自带追踪能力,可通过 assert_called_*
系列方法验证。
例子:测试通知函数的调用逻辑
假设我们有一个 send_gift(user)
函数,当用户积分>100时,会调用 notify(user, "送礼物")
发送通知。
我们需要测试:当积分足够时,notify
是否被正确调用。
# 目标函数:根据积分决定是否发送通知
def notify(user, msg):print(f"通知{user}:{msg}") # 实际可能是发送短信/邮件def send_gift(user, points):if points > 100:notify(user, "送你一份礼物!") # 满足条件时调用notify# 测试:验证积分>100时,notify被正确调用
from unittest.mock import patchdef test_send_gift():# 用patch替换notify,生成一个“模拟版notify”(mock_notify)with patch("__main__.notify") as mock_notify:# 调用send_gift,传入满足条件的参数send_gift("张三", 150)# 验证notify被调用了1次mock_notify.assert_called_once()# 验证调用时的参数是("张三", "送你一份礼物!")mock_notify.assert_called_with("张三", "送你一份礼物!")test_send_gift() # 测试通过,说明调用逻辑正确
- 核心逻辑:
patch(...) as mock_obj
会将模拟对象赋值给mock_obj
,通过mock_obj
的方法(如assert_called_once()
、assert_called_with(...)
)验证调用情况。
三、模拟外部依赖(文件、网络、数据库)
测试中若涉及外部资源(如读文件、发HTTP请求、查数据库),直接调用会导致测试不稳定(比如文件不存在、网络波动)。用 patch
替换这些外部依赖的接口,可彻底隔离环境。
例子:测试读取文件的函数
假设我们有一个 read_first_line(file_path)
函数,用于读取文件第一行。直接测试需要真实文件,用 patch
模拟 open
函数,避免依赖真实文件。
# 目标函数:读取文件第一行
def read_first_line(file_path):with open(file_path, "r") as f:return f.readline().strip()# 测试:用patch模拟open函数,返回预设内容
from unittest.mock import patch, mock_opendef test_read_first_line():# 模拟文件内容:第一行是"hello world"mock_file_content = "hello world\nsecond line"# 用mock_open创建一个模拟的open函数,返回上述内容mock_file = mock_open(read_data=mock_file_content)# 用patch替换内置的open函数,指向mock_filewith patch("builtins.open", mock_file):# 调用目标函数(此时open是模拟的,不依赖真实文件)result = read_first_line("任意路径.txt")# 验证结果是否正确assert result == "hello world"# 验证open是否以"r"模式打开了目标路径mock_file.assert_called_with("任意路径.txt", "r")test_read_first_line() # 测试通过,不依赖真实文件
- 扩展:类似地,可模拟
requests.get
来测试网络请求逻辑(比如模拟API返回的JSON数据),无需真实发请求。
四、替换类的实例或方法
当测试涉及复杂类的实例(比如数据库连接类、第三方工具类)时,用 patch
替换类的方法或直接替换实例,避免创建真实实例的开销或复杂性。
例子:测试依赖数据库连接的函数
假设 get_user_name(db_conn, user_id)
函数需要通过数据库连接 db_conn
查询用户名。我们用 patch
模拟 db_conn
的 query
方法,返回固定结果。
# 目标函数:通过数据库连接查询用户名
def get_user_name(db_conn, user_id):result = db_conn.query(f"SELECT name FROM users WHERE id={user_id}")return result[0]["name"] if result else "未知用户"# 测试:模拟数据库连接的query方法
from unittest.mock import Mock, patchdef test_get_user_name():# 创建一个模拟的数据库连接对象(Mock实例)mock_db_conn = Mock()# 设定模拟的query方法返回值:当查询id=1时,返回[{"name": "张三"}]mock_db_conn.query.return_value = [{"name": "张三"}]# 调用目标函数(传入模拟的db_conn)result = get_user_name(mock_db_conn, 1)# 验证结果assert result == "张三"# 验证query是否被传入了正确的SQLmock_db_conn.query.assert_called_with("SELECT name FROM users WHERE id=1")test_get_user_name() # 测试通过,无需真实数据库
- 这里没有用
patch
语句,而是直接创建Mock
对象(patch
本质也是生成Mock
对象),适合替换类实例的场景。
五、总结:patch
的核心应用场景
应用场景 | 核心目的 | 关键参数/方法 |
---|---|---|
模拟异常 | 验证错误处理逻辑 | side_effect=异常对象 |
替换返回值 | 控制依赖函数的输出,稳定测试结果 | return_value=固定值 |
验证调用情况 | 确认函数被正确调用(次数、参数) | assert_called_* 系列方法 |
模拟外部依赖 | 隔离文件、网络、数据库等不稳定资源 | 替换 open /requests.get 等 |
替换类实例/方法 | 简化复杂类的测试,避免真实实例开销 | 结合 Mock 对象使用 |
这些场景的本质都是**“用可控的模拟逻辑替代不可控的真实逻辑”**,让测试更稳定、更快、更易复现。无论是单元测试还是集成测试,patch
都是隔离依赖、验证行为的核心工具。