如何在不修改循环逻辑的情况下Mock列表中的布尔返回函数?
解决pytest-mocker无法Mock列表中函数返回值的问题
问题背景
我有一个返回布尔值的函数列表,代码中通过循环调用这些函数:
for f in functions: if f(): pass
编写测试时,尝试用pytest的mocker工具Mock这些函数的返回值,示例代码如下:
@pytest.fixture def is_first_func_mock(mocker): mock = mocker.patch.object(SomeClass, 'first_func') return mock
但在测试用例中设置is_first_func_mock.return_value = True后,Mock并未生效。原因是代码并非直接调用类/模块上的函数,而是调用列表中保存的函数引用。
曾看到一种解决方案是修改调用方式为:
if getattr(funcs_module, func.__name__)(): pass
但我不想修改原循环逻辑。
补充装饰器实现代码:
def some_name(functions: list[callable]): def inner(func): @wraps(func) async def wrapper(*args, **kwargs): # 一些前置代码 for f in functions: if f(): return await func(*args, **kwargs) raise SomePermission return wrapper return inner
问题原因
核心问题在于装饰器初始化时已经捕获了函数列表中每个函数的原始引用。当你用mocker.patch.object替换SomeClass.first_func时,只是修改了类属性的指向,但装饰器列表里保存的还是初始化时拿到的原始函数对象,循环调用时依然会执行原始函数,导致Mock不生效。
解决方案
方案1:直接替换列表中的函数为Mock对象
如果装饰器是基于某个固定的函数列表初始化的,你可以在测试中直接修改这个列表,把目标函数替换成Mock对象:
def test_permission_check(mocker): # 创建Mock并设置返回值 first_func_mock = mocker.Mock(return_value=True) # 导入装饰器使用的函数列表(替换成你的实际模块路径) from your_app.permissions import permission_functions # 遍历列表,找到目标函数并替换 for idx, func in enumerate(permission_functions): if func.__name__ == 'first_func': permission_functions[idx] = first_func_mock # 执行被装饰的函数,此时循环会调用Mock函数 result = await your_decorated_function() # 断言逻辑 assert result == expected_value
方案2:在装饰器应用前Patch目标函数
如果装饰器是在模块加载阶段就直接应用到函数上的(比如模块级别的装饰器),你需要先Patch目标函数,再导入使用装饰器的模块,这样装饰器初始化时拿到的就是Mock的引用:
def test_permission_check(mocker): # 先Patch目标函数,设置返回值 mocker.patch.object(SomeClass, 'first_func', return_value=True) # 再导入被装饰的函数(确保导入在Patch之后) from your_app.views import protected_function # 执行测试逻辑 response = await protected_function() assert response.status_code == 200
内容的提问来源于stack exchange,提问作者Kirill Stepankov
相关产品推荐
相关产品推荐

