【问题标题】:Pytest monkeypatch isn't working on imported functionPytest monkeypatch 不适用于导入的函数
【发布时间】:2015-07-09 00:18:37
【问题描述】:

假设一个项目中有两个包: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 中起作用?

【问题讨论】:

    标签: python unit-testing pytest


    【解决方案1】:

    虽然Ronny's answer 有效,但它会强制您更改应用程序代码。一般来说,您不应该为了测试而这样做。

    相反,您可以显式修补第二个包中的对象。 docs for the unittest module中提到了这一点。

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

    【讨论】:

    • 这显然是猴子修补导入的更简洁的方法。
    • 据我所知,该链接指向错误的文档。应该是the pytest monkeypatch docs
    • @LondonRob 单元测试文档解释了为什么打补丁是个好主意
    • 在更改代码可能是个好主意时,让我分享一个示例:您有一些广泛使用的函数并编写了一些组件/集成测试。您需要对其使用的每个模型进行monkeypatch。 monkeypatch.setattr('a.val', None); monkeypatch.setattr('b.val', None)
    • @Cjkjvfnby 我明白你的意思,我确实面临你描述的确切问题。但是,我个人认为这一点仍然存在,在测试中使用多个monkeypatch 语句而不是仅仅为了测试而修改代码结构会更好。一个例外是,如果编写测试实际上突出显示代码写得不好,在这种情况下,它会证明重写是合理的。
    【解决方案2】:

    作为Alex said,,您不应该为测试重写代码。我遇到的问题是要修补的路径。

    给定代码:

    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)
    

    虽然给出的答案已经很好,但我希望这会带来更完整的背景。

    【讨论】:

    • check_user 可用于许多文件。你是说我们每次都需要修补不同的路径?
    • 这是一个小时搜索后的救命稻草。谢谢!
    • 我认为这是与@Alex 重复的答案,但仍然给了你一个支持,所以这个解释让它在我的脑海中更好一点,谢谢!
    【解决方案3】:

    命名导入为对象创建一个新名称。如果您随后替换对象的旧名称,则新名称不受影响。

    导入模块并改用module.bar。这将始终使用当前对象。


    编辑:

    import module 
    
    def func_under_test():
      module.foo()
    
    def test_func():
       monkeypatch.setattr(...)
       func_under_test
    

    【讨论】:

    • 这是 pytest 最糟糕的问题之一——但感谢您的解释。
    • 当你说“使用module.bar”时,你能提供一个代码示例吗?我试过monkeypatch.setattr(module, 'bar', mock_obj) 和其他一些咒语都没有成功。
    • 感谢您的回答。有什么技巧可以确保任何导入都将使用模拟对象。因为现在如果我试图模拟 orm 对象是很危险的
    • 这是python语言的一个属性,没有技巧
    【解决方案4】:

    OP 问题的正确答案:

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

    【讨论】:

      【解决方案5】:

      您的函数可能没有得到修补的另一个可能原因是您的代码是否使用了多处理。

      在 macOS 上,新进程的默认启动方法已从 fork 更改为 spawn。如果使用spawn,则会启动一个全新的 Python 解释器进程,忽略您最近修补的函数。

      修复:设置默认启动方式为fork

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

      您可以将此 sn-p 添加到您的 tests/ 文件夹中的 conftest.py

      【讨论】:

        猜你喜欢
        • 2022-01-02
        • 1970-01-01
        • 2017-11-23
        • 1970-01-01
        • 2012-06-26
        • 2020-12-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多