Python:在不导入或不需要模块存在的情况下模拟模块

20 投票
2 回答
10776 浏览
提问于 2025-04-16 12:43

我开始使用一个叫做 Python 的 mock 库 来进行测试。我想要模拟一个在被测试模块的命名空间中导入的模块,而不是真的导入它,也不需要它事先存在(也就是说,不想出现导入错误)。

假设有以下代码:

foo.py

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

我的目标是测试 foo_func(),即使 'helpers.py' 根本不存在,如果它存在,也要表现得好像它不存在一样。

第一次尝试,使用超级酷的 @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

如果可以导入 "helpers" 模块,这个方法是有效的。但如果它不存在,我就会遇到导入错误。

接下来尝试使用 patch,但不加装饰器:

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

同样,如果 "helpers" 不存在,这个方法也不行……而且如果它存在,断言会失败。我也不太明白为什么会这样。

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

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" 是否存在都能通过。

这样做是实现这个目标的最佳方式吗?我使用的 mock 库有没有其他的替代方法?还有什么其他方法可以做到这一点?

2 个回答

3

我遇到过类似的问题,helpers这个库无法加载,因为它需要特别的硬件。与其对你想测试的代码进行大幅度修改,不如按照如何为测试添加包到sys路径的建议,插入一个“假”的目录到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()
6

你有点没理解什么是Mock。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()

标准库中的很多部分都是这样工作的。这几乎是定义单例模块的标准做法。

撰写回答