Pytest monkeypatch 不适用于导入的函数

Pytest monkeypatch isn't working on imported function

假设一个项目中有两个包:some_packageanother_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