python单元测试 unittest.mock.patch (一)
当然可以!我们完全脱离AI场景,用最基础的Python语法和unittest.mock
模块知识,来讲解“模拟异常”的核心原理和过程。重点理解:如何通过代码“主动制造错误”,以及错误是如何被触发和处理的。
一、核心工具:unittest.mock.patch
是什么?
unittest.mock
是Python标准库中用于**“模拟”**(替换)对象的工具,而patch
是其中最常用的功能。它的本质是:
在一段代码执行期间,临时替换某个函数/方法/对象的行为,执行完后自动恢复原状。
类比:相当于给某个函数“贴个临时补丁”,让它在特定场景下按我们的要求工作,补丁用完就撕掉,不影响原函数。
二、模拟异常的核心:side_effect
参数
patch
有个关键参数side_effect
,专门用来定制被替换对象的“输出”或“行为”。当我们把side_effect
设为一个异常对象时,就实现了“模拟异常”——被替换的函数被调用时,不会执行原逻辑,而是直接抛出这个异常。
最简单的例子:模拟一个函数抛出异常
假设我们有一个普通函数divide(a, b)
,功能是计算a/b
,但当b=0
时会自然抛出ZeroDivisionError
。现在我们想测试:当这个函数“被迫”抛出其他异常(比如TypeError
)时,调用者是否能正确处理。
# 目标函数:正常的除法逻辑
def divide(a, b):return a / b# 测试场景:我们想让divide函数调用时,主动抛出TypeError(而不是自然报错)
from unittest.mock import patch# 1. 定义要模拟的异常(TypeError类型,错误信息为"模拟类型错误")
mock_error = TypeError("模拟类型错误")# 2. 用patch替换divide函数,设置side_effect=mock_error
with patch("__main__.divide", side_effect=mock_error):# 3. 在这个代码块内,调用divide时会触发我们定义的异常try:result = divide(10, 2) # 这里调用的已经是被替换后的divideexcept TypeError as e:print(f"捕获到异常:{e}") # 会打印:捕获到异常:模拟类型错误# 4. 代码块结束后,divide恢复原状
print(divide(10, 2)) # 正常执行,输出5.0
过程拆解:异常是如何触发的?
-
替换阶段:
with patch("__main__.divide", side_effect=mock_error):
这行代码会:- 找到当前模块(
__main__
)中的divide
函数; - 用一个“模拟函数”临时替换它,这个模拟函数的行为被
side_effect=mock_error
定义。
- 找到当前模块(
-
调用阶段:当执行
divide(10, 2)
时:- 实际调用的是“模拟函数”,而非原函数;
- 模拟函数一被调用,就会直接抛出
mock_error
(即TypeError("模拟类型错误")
)。
-
捕获阶段:抛出的异常被
try...except
捕获,执行except
块中的逻辑(打印错误信息)。 -
恢复阶段:
with
代码块执行结束后,“模拟函数”被移除,divide
恢复成原来的除法逻辑(所以最后一行能正常计算10/2
)。
三、关键细节:被替换对象的“路径”怎么写?
patch
的第一个参数是被替换对象的“完整路径”,这是最容易出错的地方。路径的写法要遵循:“从调用者的视角能找到该对象的路径”。
比如上面的例子中,divide
函数定义在当前模块(__main__
),所以路径是"__main__.divide"
。再举一个更清晰的例子:
# 假设我们有一个文件math_functions.py,内容如下:
def add(a, b):return a + b
现在在另一个文件test.py中测试,想替换add
函数:
# test.py
from math_functions import add
from unittest.mock import patch# 正确的路径:从test.py的视角,add来自math_functions模块
with patch("math_functions.add", side_effect=ValueError("模拟值错误")):try:add(1, 2)except ValueError as e:print(f"捕获到:{e}") # 输出:捕获到:模拟值错误
如果路径写错(比如写成"test.add"
),patch
会找不到对象,导致模拟失败。
四、为什么要“模拟异常”?测试中的核心用途
模拟异常的本质是**“主动制造可控的错误场景”**,用于验证代码的“错误处理逻辑是否可靠”。
比如我们写了一个带错误处理的函数safe_divide
:
def safe_divide(a, b):try:return divide(a, b)except ZeroDivisionError:return "除数不能为0"except TypeError:return "参数必须是数字"except Exception:return "未知错误"
现在要测试:当divide
抛出TypeError
时,safe_divide
是否会返回“参数必须是数字”。
如果不模拟异常,我们只能通过传入非数字参数(如divide(10, "a")
)触发TypeError
,但这种方式有局限性(比如有些异常很难自然触发)。
用模拟异常就很简单:
def test_safe_divide():# 模拟divide抛出TypeErrorwith patch("__main__.divide", side_effect=TypeError):result = safe_divide(10, 2) # 这里divide被替换,会抛TypeErrorassert result == "参数必须是数字" # 验证错误处理是否正确test_safe_divide() # 测试通过,说明逻辑可靠
五、总结:模拟异常的核心流程
- 定义异常:创建一个要模拟的异常对象(如
TypeError("测试错误")
)。 - 替换目标:用
with patch(目标路径, side_effect=异常对象):
临时替换目标函数。 - 触发异常:在
with
块中调用被替换的函数,此时会直接抛出我们定义的异常。 - 验证处理:检查代码是否按预期捕获并处理了这个异常(如进入对应的
except
块)。 - 自动恢复:
with
块结束后,目标函数恢复原状,不影响其他代码。
核心原理就是:通过“替换”让函数“按我们的要求报错”,从而验证错误处理逻辑。这种技术和AI无关,是Python单元测试中验证代码健壮性的基础手段。