You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

原因分析

  1. 装饰器的执行逻辑
    当你用@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,再返回原始函数的结果
  2. 当前Monkeypatch的问题
    你用monkeypatch.setattr(sys.modules[__name__], 'show_number', mocked_show_number)替换的是测试模块中导入的show_number变量,直接把它换成了mock函数。此时调用show_number(2)会直接执行mock,完全绕过了原来的wrapper(装饰器逻辑),所以返回5是正常的。

  3. 你的期望误区
    你期望结果为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 09:25:19