使用Pytest MonkeyPatch装饰方法时替换了装饰器,这是正常行为吗?
pytest中Monkeypatch带装饰器函数的问题分析
你遇到的情况是预期行为,但你的Monkeypatch方式不符合需求,同时对装饰器的执行逻辑存在误解,下面详细分析并给出解决方案:
问题代码回顾
functions.py
def mul_by(func): def wrapper(num): return func(num * 4) return wrapper @mul_by def show_number(n: int): print(n) return n
test_functions.py
import sys from src.functions import show_number def test_show_number(monkeypatch): def mocked_show_number(num: int): print('Mocked func') return 5 monkeypatch.setattr(sys.modules[__name__], 'show_number', mocked_show_number) assert show_number(2) == 20
输出结果
collected 1 item test/test_functions.py F [100%] ================================================================================================= FAILURES ================================================================================================= _____________________________________________________________________________________________ test_show_number _____________________________________________________________________________________________ monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10b7e5d00> def test_show_number(monkeypatch): def mocked_show_number(num: int): print('Mocked func') return 5 monkeypatch.setattr(sys.modules[__name__], 'show_number', mocked_show_number) > assert show_number(2) == 20 E assert 5 == 20 E + where 5 = show_number(2) test/test_functions.py:11: AssertionError ------------------------------------------------------------------------------------------- Captured stdout call ------------------------------------------------------------------------------------------- Mocked func ========================================================================================= short test summary info ========================================================================================== FAILED test/test_functions.py::test_show_number - assert 5 == 20
原因分析
装饰器的执行逻辑
当你用@mul_by装饰show_number时,等价于执行了show_number = mul_by(show_number):- 原始的
show_number函数被传入mul_by mul_by返回wrapper函数,此时模块中的show_number已经变成了这个wrapper- 调用
show_number(2)时,实际执行的是wrapper(2):先把2*4=8传给原始show_number,再返回原始函数的结果
- 原始的
当前Monkeypatch的问题
你用monkeypatch.setattr(sys.modules[__name__], 'show_number', mocked_show_number)替换的是测试模块中导入的show_number变量,直接把它换成了mock函数。此时调用show_number(2)会直接执行mock,完全绕过了原来的wrapper(装饰器逻辑),所以返回5是正常的。你的期望误区
你期望结果为20,是误以为装饰器会把mock的返回值再乘4,但实际装饰器的逻辑是先将输入参数乘4,再传给目标函数,所以即使正确补丁,mock返回5的话,最终结果也是5,而非20。
正确解决方案
方案1:修改原代码(推荐)
在装饰器中使用functools.wraps,它会帮我们保留原始函数的引用(通过__wrapped__属性):
修改后的functions.py:
import functools def mul_by(func): @functools.wraps(func) def wrapper(num): return func(num * 4) return wrapper @mul_by def show_number(n: int): print(n) return n
修改后的测试代码:
from src.functions import show_number def test_show_number(monkeypatch): def mocked_show_number(num: int): print('Mocked func') return 5 # 补丁被装饰器包裹的原始函数 monkeypatch.setattr(show_number, '__wrapped__', mocked_show_number) result = show_number(2) # wrapper会把2*4=8传给mock,mock返回5,所以结果为5 assert result == 5
方案2:不修改原代码
通过inspect模块获取闭包中的原始函数并补丁,这种方式依赖闭包结构,稳定性稍差:
from src.functions import show_number import inspect def test_show_number(monkeypatch): def mocked_show_number(num: int): print('Mocked func') return 5 # 获取wrapper闭包中的原始func变量 closure_info = inspect.getclosurevars(show_number) # 定位闭包中的func变量(装饰器参数名) func_cell = [cell for cell in show_number.__closure__ if cell.cell_contents == closure_info.nonlocals['func']][0] # 补丁该闭包变量的调用逻辑 monkeypatch.setattr(func_cell.cell_contents, '__call__', mocked_show_number) result = show_number(2) assert result == 5
总结
- 你当前的Monkeypatch结果是预期行为,因为直接替换了整个被装饰后的函数,绕过了装饰器逻辑。
- 要让装饰器生效,需要补丁的是被装饰器包裹的原始函数,而非替换整个被装饰后的函数。
- 需明确装饰器的执行顺序:原装饰器是先处理输入参数,再传给目标函数,而非处理目标函数的返回值。
内容的提问来源于stack exchange,提问作者Ander2
相关产品推荐
相关产品推荐

