Pytest monkeypatch 不适用于导入的函数
Pytest monkeypatch isn't working on imported function
假设一个项目中有两个包:some_package
和another_package
。
# some_package/foo.py:
def bar():
print('hello')
# another_package/function.py
from some_package.foo import bar
def call_bar():
# ... code ...
bar()
# ... code ...
我想测试 another_package.function.call_bar
模拟 some_package.foo.bar
因为它有一些我想避免的网络 I/O。
这是一个测试:
# tests/test_bar.py
from another_package.function import call_bar
def test_bar(monkeypatch):
monkeypatch.setattr('some_package.foo.bar', lambda: print('patched'))
call_bar()
assert True
令我惊讶的是它输出 hello
而不是 patched
。我试图调试这个东西,在测试中放置了一个 IPDB 断点。当我在断点后手动导入 some_package.foo.bar
并调用 bar()
时,我得到 patched
.
在我的真实项目中,情况更加有趣。如果我在项目根目录中调用 pytest,我的函数没有被修补,但是当我指定 tests/test_bar.py
作为参数时 - 它有效。
据我了解,它与 from some_package.foo import bar
语句有关。如果它在 monkeypatching 发生之前执行,则修补失败。但是在上述示例的压缩测试设置中,补丁在这两种情况下都不起作用。
为什么它在 IPDB REPL 中遇到断点后仍然有效?
命名导入为对象创建一个新名称。如果您随后替换对象的旧名称,新名称不受影响。
导入模块并改用 module.bar
。这将始终使用当前对象。
编辑:
import module
def func_under_test():
module.foo()
def test_func():
monkeypatch.setattr(...)
func_under_test
虽然 有效,但它会强制您更改应用程序代码。一般来说,你不应该为了测试而这样做。
相反,您可以显式修补第二个包中的对象。 docs for the unittest module.
中提到了这一点
monkeypatch.setattr('another_package.bar', lambda: print('patched'))
OP 问题的正确答案:
monkeypatch.setattr('another_package.function.bar', lambda: print('patched'))
作为 ,您不应该为测试重写代码。我 运行 遇到的问题是修补的路径。
给定代码:
app/handlers/tasks.py
from auth.service import check_user
def handle_tasks_create(request):
check_user(request.get('user_id'))
create_task(request.body)
return {'status': 'success'}
你对 monkeypatch check_user
的第一直觉,就像这样:
monkeypatch.setattr('auth.service.check_user', lambda x: return None)
但是您要做的是修补 tasks.py
中的实例。可能这就是您想要的:
monkeypatch.setattr('app.handlers.tasks.check_user', lambda x: return None)
虽然给出的答案已经很好,但我希望这能带来更完整的上下文。
您的函数未得到修补的另一个可能原因是您的代码正在使用多处理。
在 macOS 上,新进程的默认启动方法已从 fork
更改为 spawn
。如果使用 spawn
,则会启动一个全新的 Python 解释器进程,忽略您最近修补的函数。
修复:将默认启动方法设置为 fork
。
import multiprocessing
multiprocessing.set_start_method('fork', force=True)
您可以将此代码段添加到 tests/
文件夹内的 conftest.py
。
假设一个项目中有两个包:some_package
和another_package
。
# some_package/foo.py:
def bar():
print('hello')
# another_package/function.py
from some_package.foo import bar
def call_bar():
# ... code ...
bar()
# ... code ...
我想测试 another_package.function.call_bar
模拟 some_package.foo.bar
因为它有一些我想避免的网络 I/O。
这是一个测试:
# tests/test_bar.py
from another_package.function import call_bar
def test_bar(monkeypatch):
monkeypatch.setattr('some_package.foo.bar', lambda: print('patched'))
call_bar()
assert True
令我惊讶的是它输出 hello
而不是 patched
。我试图调试这个东西,在测试中放置了一个 IPDB 断点。当我在断点后手动导入 some_package.foo.bar
并调用 bar()
时,我得到 patched
.
在我的真实项目中,情况更加有趣。如果我在项目根目录中调用 pytest,我的函数没有被修补,但是当我指定 tests/test_bar.py
作为参数时 - 它有效。
据我了解,它与 from some_package.foo import bar
语句有关。如果它在 monkeypatching 发生之前执行,则修补失败。但是在上述示例的压缩测试设置中,补丁在这两种情况下都不起作用。
为什么它在 IPDB REPL 中遇到断点后仍然有效?
命名导入为对象创建一个新名称。如果您随后替换对象的旧名称,新名称不受影响。
导入模块并改用 module.bar
。这将始终使用当前对象。
编辑:
import module
def func_under_test():
module.foo()
def test_func():
monkeypatch.setattr(...)
func_under_test
虽然
相反,您可以显式修补第二个包中的对象。 docs for the unittest module.
中提到了这一点monkeypatch.setattr('another_package.bar', lambda: print('patched'))
OP 问题的正确答案:
monkeypatch.setattr('another_package.function.bar', lambda: print('patched'))
作为
给定代码:
app/handlers/tasks.py
from auth.service import check_user
def handle_tasks_create(request):
check_user(request.get('user_id'))
create_task(request.body)
return {'status': 'success'}
你对 monkeypatch check_user
的第一直觉,就像这样:
monkeypatch.setattr('auth.service.check_user', lambda x: return None)
但是您要做的是修补 tasks.py
中的实例。可能这就是您想要的:
monkeypatch.setattr('app.handlers.tasks.check_user', lambda x: return None)
虽然给出的答案已经很好,但我希望这能带来更完整的上下文。
您的函数未得到修补的另一个可能原因是您的代码正在使用多处理。
在 macOS 上,新进程的默认启动方法已从 fork
更改为 spawn
。如果使用 spawn
,则会启动一个全新的 Python 解释器进程,忽略您最近修补的函数。
修复:将默认启动方法设置为 fork
。
import multiprocessing
multiprocessing.set_start_method('fork', force=True)
您可以将此代码段添加到 tests/
文件夹内的 conftest.py
。