简体   繁体   English

Pytest monkeypatch 不适用于导入的函数

[英]Pytest monkeypatch isn't working on imported function

Suppose there are two packages in a project: some_package and another_package .假设一个项目中有两个包: 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 ...

I want to test another_package.function.call_bar mocking out some_package.foo.bar because it has some network I/OI want to avoid.我想测试another_package.function.call_bar some_package.foo.bar因为它有一些网络 I/OI 想要避免。

Here is a test:这是一个测试:

# 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

To my surprise it outputs hello instead of patched .令我惊讶的是,它输出hello而不是patched I tried to debug this thing, putting an IPDB breakpoint in the test.我试图调试这个东西,在测试中放置了一个IPDB断点。 When I manually import some_package.foo.bar after the breakpoint and call bar() I get patched .当我手动导入some_package.foo.bar断点和呼叫后bar()我得到patched

On my real project the situation is even more interesting.在我的真实项目中,情况更有趣。 If I invoke pytest in the project root my function isn't patched, but when I specify tests/test_bar.py as an argument - it works.如果我在项目根目录中调用 pytest 我的函数没有被修补,但是当我指定tests/test_bar.py作为参数时 - 它可以工作。

As far as I understand it has something to do with the from some_package.foo import bar statement.据我了解,它与from some_package.foo import bar语句有关。 If it's being executed before monkeypatching is happening then patching fails.如果在发生monkeypatching 之前执行它,则修补失败。 But on the condensed test setup from the example above patching does not work in both cases.但是在上面示例中的精简测试设置中,修补在两种情况下都不起作用。

And why does it work in IPDB REPL after hitting a breakpoint?为什么它在遇到断点后可以在 IPDB REPL 中工作?

While Ronny's answer works it forces you to change application code.虽然Ronny 的回答有效,但它迫使您更改应用程序代码。 In general you should not do this for the sake of testing.一般来说,您不应该为了测试而这样做。

Instead you can explicitly patch the object in the second package.相反,您可以明确修补第二个包中的对象。 This is mentioned in the docs for the unittest module .在 unittest 模块文档中提到。

monkeypatch.setattr('another_package.bar', lambda: print('patched'))

Named importation creates a new name for the object.命名导入为对象创建一个新名称。 If you then replace the old name for the object the new name is unaffected.如果随后替换对象的旧名称,则新名称不受影响。

Import the module and use module.bar instead.导入模块并使用module.bar代替。 That will always use the current object.那将始终使用当前对象。


EDIT:编辑:

import module 

def func_under_test():
  module.foo()

def test_func():
   monkeypatch.setattr(...)
   func_under_test

As Alex said, you shouldn't rewrite your code for your tests.正如亚历克斯所说,你不应该为你的测试重写你的代码。 The gotcha I ran into is which path to patch.我遇到的问题是要修补的路径。

Given the code:鉴于代码:

app/handlers/tasks.py应用程序/处理程序/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'}

Your first instinct to monkeypatch check_user , like this:你对monkeypatch check_user第一直觉,像这样:

monkeypatch.setattr('auth.service.check_user', lambda x: return None)

But what you want to do is patch the instance in tasks.py .但是您要做的是修补tasks.py的实例。 Likely this is what you want:可能这就是你想要的:

monkeypatch.setattr('app.handlers.tasks.check_user', lambda x: return None)

While the answers given are already good, I hope this brings more complete context.虽然给出的答案已经很好,但我希望这能带来更完整的背景。

OP问题的正确答案:

monkeypatch.setattr('another_package.function.bar', lambda: print('patched'))

Another possible reason your function may not be getting patched is if your code is using multiprocessing.您的函数可能未被修补的另一个可能原因是您的代码是否使用了多处理。

On macOS the default start method for a new process has changed from fork to spawn .在 macOS 上,新进程的默认启动方法已从fork更改为spawn If spawn is used, a brand new Python interpreter process is started, ignoring your recently patched function.如果使用spawn ,则会启动一个全新的 Python 解释器进程,忽略您最近修补的函数。

Fix: Set the default start method to fork .修复:将默认启动方法设置为fork

import multiprocessing

multiprocessing.set_start_method('fork', force=True)

You can add this snippet to conftest.py inside your tests/ folder.您可以将此代码段添加到tests/文件夹中的conftest.py

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM