【问题标题】:Python: Mock a module without importing it or needing it to existPython:模拟一个模块而不导入它或不需要它存在
【发布时间】:2025-11-27 14:35:01
【问题描述】:

我开始使用 python mock library 进行测试。我想模拟一个在被测模块的命名空间中导入的模块,而不实际导入它或要求它首先存在(即抛出 ImportError)。

假设存在以下代码:

foo.py

 import helpers
 def foo_func():
    return helpers.helper_func()

目标是在“helpers.py”在任何地方都不存在时测试 foo_func(),如果它存在,就当它不存在。

第一次尝试,使用超酷的@patch 装饰器:

from mock import patch, sentinel
import foo
@patch("foo.helpers")
def foo_test(mock):
    mock.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

如果可以导入“帮助器”模块,则此方法有效。如果它不存在,我会收到 ImportError。

下一次尝试使用补丁,无装饰器:

from mock import patch, sentinel, Mock
import foo
helpers_mock = patch("foo.helpers")
helpers_mock.start()

def foo_test():
    helpers_mock.helper_func = Mock('helper_func')
    helpers_mock.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

同样,如果缺少“帮助者”,这将不起作用......并且,如果存在,则断言失败。不太清楚为什么会这样。

第三次尝试,当前解决方案:

import sys
helpers_mock = Mock(name="helpers_mock", spec=['helper_func'])
helpers_mock.__name__ = 'helpers'
sys.modules['helpers'] = helpers_mock
import foo
def foo_test():
    helpers_mock.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

无论“helpers.py”是否存在,此测试都会通过。

这是实现这一目标的最佳方式吗?我正在使用的模拟库是否提供了替代方案?我还有什么其他方法可以做到这一点?

【问题讨论】:

  • 虽然我完全支持单元测试,但我认为在这种情况下,您正在测试语言的一个特性,而不是代码的有用功能。您还可以在 foo.py 中修复您的导入,以便使用导入保护不需要某些猴子修补(例如:尝试:导入助手,除了 ImportError:foo.py 中的 helpers = None 而不是裸导入)。我的意思是现在 foo.py 不是用 a 编写的,以允许缺少帮助程序,并且允许这样做应该会大大简化您的测试。
  • 我真的很喜欢 import guard 的想法,它解决了很多问题。如果我按照您的建议使用导入保护,我可以使用我给出的第一个示例(@patch 装饰器)。谢谢!

标签: python module import mocking


【解决方案1】:

您有点忽略了 Mock 是什么。当你想要一个具有特定接口的对象时,你应该构建它们,不管它是如何实现的。

你正在做的是试图重新实现 python 的模块系统,而且这是对全局变量的非常可怕的滥用。

不要让 foo 成为一个模块,而是创建一个 Foo 类,并在构造函数中传入帮助器。

class Foo(object):
    def __init__(self, helpers):
        self.helpers = helpers

# then, instead of import foo:
foo = Foo(mock_helpers)

即使真正的“帮助者”实际上是一个模块,也没有理由在测试中使用 sys.modules 并通过 import 设置它。

如果 foo 必须是一个模块,那也没关系,但你可以这样做:

# foo.py
class Foo(object):
    pass # same code as before, plus foo_func

try:
   import whatever
   _singleton = Foo(whatever)
except ImportError:
   _singleton = Foo(something_else)

def foo_func():
   return _singleton.foo_func()

标准库的大部分都是这样工作的。这几乎是定义单例模块的​​标准。

【讨论】:

  • 这是一个非常好的信息,不幸的是这意味着要重写很多代码,目前这并不实用。但对于新项目,我一定会尝试这种方法。
【解决方案2】:

我遇到了类似的问题,因为它需要特殊的硬件,所以无法加载 helpers 库。 与其对要测试的代码进行根本性更改,不如按照how to add a package to sys path for testing 的建议将“假”目录插入sys.path

import os, sys
fake_dir = os.path.join(os.path.dirname(__file__), 'fake')
assert(os.path.exists(fake_dir))
sys.path.insert(0, fake_dir)
import foo
from unittest.mock import sentinel
def foo_test():
    foo.helpers.helper_func.return_value = sentinel.foobar
    assert foo.foo_func() == sentinel.foobar

fake 的结构如下:

.
├── fake/
│   └── helpers/
│       └── __init__.py
├── foo.py
└── helpers/

__init__.py

from unittest.mock import Mock
helper_func = Mock()

【讨论】:

  • 破解包含路径虽然很难看,但这是我发现模拟单元测试环境中不可用或无法加载的模块的唯一方法。
最近更新 更多