Python:在不导入或不需要模块存在的情况下模拟模块
我开始使用一个叫做 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 个回答
我遇到过类似的问题,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()
你有点没理解什么是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()
标准库中的很多部分都是这样工作的。这几乎是定义单例模块的标准做法。