Python模拟从导入的模块中调用函数

227

我希望了解如何从已导入的模块中修补一个函数。

目前我所在的位置是这样的:

app/mocking.py:

from app.my_module import get_user_name

def test_method():
  return get_user_name()

if __name__ == "__main__":
  print "Starting Program..."
  test_method()

应用程序/我的模块/__init__.py:

def get_user_name():
  return "Unmocked User"

测试/模拟测试.py:

import unittest
from app.mocking import test_method 

def mock_get_user():
  return "Mocked This Silly"

@patch('app.my_module.get_user_name')
class MockingTestTestCase(unittest.TestCase):

  def test_mock_stubs(self, mock_method):
    mock_method.return_value = 'Mocked This Silly')
    ret = test_method()
    self.assertEqual(ret, 'Mocked This Silly')

if __name__ == '__main__':
  unittest.main()

这并不像我期望的那样工作。 “patched”模块只是返回了get_user_name未经mock处理的值。我如何mock我导入到测试命名空间中的其他包的方法?


我想问一下我是否正确地处理了这个问题。我看了Mock,但是我没有找到解决这个特定问题的方法。在Mock中有没有重新创建我上面所做的事情的方法? - nsfyn55
4个回答

305
当您使用来自unittest.mock包的patch修饰符时,您正在对测试命名空间(在此例中为app.mocking.get_user_name)进行补丁,而不是从中导入函数的命名空间(在此例中为app.my_module.get_user_name)。为了做到您所描述的,请尝试以下操作:@patch
from mock import patch
from app.mocking import test_method 

class MockingTestTestCase(unittest.TestCase):
    
    @patch('app.mocking.get_user_name')
    def test_mock_stubs(self, test_patch):
        test_patch.return_value = 'Mocked This Silly'
        ret = test_method()
        self.assertEqual(ret, 'Mocked This Silly')

标准库文档中包含一个有用的部分描述了这一点。


2
这就涉及到我的问题了。get_user_name在一个不同的模块中,而test_method在另一个模块中。有没有一种方法可以在子模块中进行模拟?我下面用一种丑陋的方式解决了这个问题。 - nsfyn55
7
既然您将 get_user_name 函数导入到 app.mocking 中,那么它们就在同一个命名空间中了,因此 get_user_nametest_method 在不同的模块中并不重要。 - Matti John
4
test_patch是从哪里来的?它具体是什么? - Mike G
4
test_patch 是由装饰器 patch 传入的,它是一个被 Mock 的 get_user_name 对象(即 MagicMock 类的实例)。如果将其命名为 get_user_name_patch 或许更容易理解一些。 - Matti John
2
刚刚我遇到的一个小问题是,“from mock import patch”这一行代码必须放在第一行。如果你先导入test_method,然后再导入patch,它将无法正常工作。只是想强调一下。 - Gurce
显示剩余5条评论

26

虽然Matti John的回答解决了你的问题(也帮助了我,谢谢!),但我建议将原始的“get_user_name”函数替换为模拟函数。这样可以让你控制何时替换函数以及何时不替换。此外,这还允许你在同一个测试中进行多次替换。为了实现这一点,可以使用“with”语句以相似的方式:

from mock import patch

class MockingTestTestCase(unittest.TestCase):

    def test_mock_stubs(self):
        with patch('app.mocking.get_user_name', return_value = 'Mocked This Silly'):
            ret = test_method()
            self.assertEqual(ret, 'Mocked This Silly')

20
对于提出的问题来说,这有点不重要。无论您是将patch作为装饰器或上下文管理器使用,都取决于具体的用例。例如,您可以将patch作为装饰器在xunitpytest类中模拟所有测试的值,而在其他情况下,使用上下文管理器可以提供更精细的控制。 - nsfyn55

4
接受的答案是正确的,使用patch时必须考虑导入的命名空间。但是想象一下,如果您想要在全局范围内覆盖实际的实现,无论它在哪里被导入,您可以使用monkeypatch来实现:
@pytest.fixture(autouse=True)
def get_user_name_mock(monkeypatch):
    _mock = MagicMock()
    monkeypatch.setattr(app.my_module, app.my_module.get_user_name.__name__, _mock )
    return _mock 


在你的测试中,只需将fixture的名称作为参数添加,就像默认的fixture行为一样。
在我的项目中,我使用这个来覆盖全局配置值的解析,以捕获可能导致未定义行为的未模拟的config.get调用。
编辑:文档链接:https://pytest.org/en/7.3.x/how-to/monkeypatch.html#how-to-monkeypatch-mock-modules-and-environments

-3
除了Matti John的解决方案之外,您还可以在app/mocking.py中导入模块而不是函数。
# from app.my_module import get_user_name
from app import my_module

def test_method():
  return my_module.get_user_name()

if __name__ == "__main__":
  print "Starting Program..."
  test_method()

为什么这个被踩了?改变导入方式确实有其不足之处,但我可以想到很多情况下,这种方式可能比修补导入模块更好。 - Michał Jabłoński
1
我认为这个回答并没有清楚地解答问题。这只是对函数进行语义上的替代。问题的核心是“为什么这个模拟不按预期工作?”你的回答并没有回答这个问题。 - nsfyn55

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接