模块 A
在其顶部包含 import B
。但是在测试条件下,我想在 A
中mock B
(模拟 A.B
)并完全避免导入 B
。
事实上,B
并不是故意安装在测试环境中的。
A
是被测单元。我必须导入 A
及其所有功能。 B
是我需要模拟的模块。但是,如果 A
所做的第一件事是导入 B
,我如何在 A
中模拟 B
并阻止 A
导入真正的 B
?
(未安装 B 的原因是我使用 pypy 进行快速测试,不幸的是 B 与 pypy 尚不兼容。)
怎么可能做到这一点?
您可以在导入 A
之前分配给 sys.modules['B']
以获得您想要的:
测试.py:
import sys
sys.modules['B'] = __import__('mock_B')
import A
print(A.B.__name__)
一个.py:
import B
注意 B.py 不存在,但在运行 test.py
时不会返回错误并且 print(A.B.__name__)
会打印 mock_B
。您仍然需要创建一个 mock_B.py
,在其中模拟 B
的实际函数/变量/等。或者您可以直接分配一个 Mock()
:
测试.py:
import sys
sys.modules['B'] = Mock()
import A
内置 __import__
可以使用“模拟”库进行模拟以获得更多控制:
# Store original __import__
orig_import = __import__
# This will be the B module
b_mock = mock.Mock()
def import_mock(name, *args):
if name == 'B':
return b_mock
return orig_import(name, *args)
with mock.patch('__builtin__.__import__', side_effect=import_mock):
import A
说 A
看起来像:
import B
def a():
return B.func()
A.a()
返回 b_mock.func()
也可以模拟。
b_mock.func.return_value = 'spam'
A.a() # returns 'spam'
Python 3 的注意事项:如 changelog for 3.0 中所述,__builtin__
现在被命名为 builtins
:
将模块 __builtin__ 重命名为 builtins(删除下划线,添加一个 's')。
如果您将 Python 3 的 __builtin__
替换为 builtins
,则此答案中的代码可以正常工作。
import A
调用了 import_mock
,但它导入的任何内容都没有调用。
ImportError: No module named '__builtin__'
__builtin__
builtins
替换 python3 (docs.python.org/3/whatsnew/3.0.html?highlight=__builtin__) 的 __builtin__
如何模拟导入(模拟 AB)?模块 A 在其顶部包含导入 B。
很简单,只需在导入之前在 sys.modules 中模拟库:
if wrong_platform():
sys.modules['B'] = mock.MagicMock()
然后,只要 A
不依赖从 B 的对象返回的特定类型的数据:
import A
应该可以工作。
您还可以模拟导入 AB:
即使您有子模块,这也有效,但您需要模拟每个模块。假设你有这个:
from foo import This, That, andTheOtherThing
from foo.bar import Yada, YadaYada
from foo.baz import Blah, getBlah, boink
要模拟,只需在导入包含上述内容的模块之前执行以下操作:
sys.modules['foo'] = MagicMock()
sys.modules['foo.bar'] = MagicMock()
sys.modules['foo.baz'] = MagicMock()
(我的经验:我有一个可以在 Windows 平台上运行的依赖项,但不能在我们运行日常测试的 Linux 上运行。所以我需要为我们的测试模拟依赖项。幸运的是,它是一个黑匣子,所以我不需要设置很多交互。)
模拟副作用
附录:实际上,我需要模拟一个需要一些时间的副作用。所以我需要一个对象的方法来休眠一秒钟。这将像这样工作:
sys.modules['foo'] = MagicMock()
sys.modules['foo.bar'] = MagicMock()
sys.modules['foo.baz'] = MagicMock()
# setup the side-effect:
from time import sleep
def sleep_one(*args):
sleep(1)
# this gives us the mock objects that will be used
from foo.bar import MyObject
my_instance = MyObject()
# mock the method!
my_instance.method_that_takes_time = mock.MagicMock(side_effect=sleep_one)
然后代码需要一些时间才能运行,就像真正的方法一样。
Aaron Hall 的回答对我有用。只想提一件重要的事,
如果在 A.py
你这样做
from B.C.D import E
然后在 test.py
中,您必须沿路径模拟每个模块,否则您会得到 ImportError
sys.modules['B'] = mock.MagicMock()
sys.modules['B.C'] = mock.MagicMock()
sys.modules['B.C.D'] = mock.MagicMock()
我意识到我在这里参加聚会有点晚了,但这里有一种使用 mock
库自动执行此操作的疯狂方法:
(这是一个示例用法)
import contextlib
import collections
import mock
import sys
def fake_module(**args):
return (collections.namedtuple('module', args.keys())(**args))
def get_patch_dict(dotted_module_path, module):
patch_dict = {}
module_splits = dotted_module_path.split('.')
# Add our module to the patch dict
patch_dict[dotted_module_path] = module
# We add the rest of the fake modules in backwards
while module_splits:
# This adds the next level up into the patch dict which is a fake
# module that points at the next level down
patch_dict['.'.join(module_splits[:-1])] = fake_module(
**{module_splits[-1]: patch_dict['.'.join(module_splits)]}
)
module_splits = module_splits[:-1]
return patch_dict
with mock.patch.dict(
sys.modules,
get_patch_dict('herp.derp', fake_module(foo='bar'))
):
import herp.derp
# prints bar
print herp.derp.foo
这如此复杂的原因是当导入发生时,python 基本上是这样做的(例如 from herp.derp import foo
)
sys.modules['herp'] 是否存在?否则导入它。如果仍然没有 ImportError sys.modules['herp.derp'] 是否存在?否则导入它。如果仍然没有 ImportError 获取 sys.modules['herp.derp'] 的属性 foo。否则 ImportError foo = sys.modules['herp.derp'].foo
这种破解在一起的解决方案有一些缺点:如果其他东西依赖于模块路径中的其他东西,那么这种情况就会被搞砸。此外,这仅适用于内联导入的内容,例如
def foo():
import herp.derp
或者
def foo():
__import__('herp.derp')
我找到了在 Python 中模拟导入的好方法。这是 Eric 的 Zaadi 解决方案 here,我只是在我的 Django 应用程序中使用它。
我有 SeatInterface
类,它是 Seat
模型类的接口。所以在我的 seat_interface
模块中,我有这样一个导入:
from ..models import Seat
class SeatInterface(object):
(...)
我想为 SeatInterface
类创建隔离测试,并将模拟的 Seat
类作为 FakeSeat
。问题是 - 如何离线运行测试,Django 应用程序已关闭。我有以下错误:
ImproperlyConfigured:请求设置 BASE_DIR,但未配置设置。您必须在访问设置之前定义环境变量 DJANGO_SETTINGS_MODULE 或调用 settings.configure()。在 0.078 秒内运行 1 次测试失败(错误 = 1)
解决方案是:
import unittest
from mock import MagicMock, patch
class FakeSeat(object):
pass
class TestSeatInterface(unittest.TestCase):
def setUp(self):
models_mock = MagicMock()
models_mock.Seat.return_value = FakeSeat
modules = {'app.app.models': models_mock}
patch.dict('sys.modules', modules).start()
def test1(self):
from app.app.models_interface.seat_interface import SeatInterface
然后测试神奇地运行正常:)
.在 0.002 秒内运行 1 次测试 OK
如果您执行 import ModuleB
,您实际上将内置方法 __import__
调用为:
ModuleB = __import__('ModuleB', globals(), locals(), [], -1)
您可以通过导入 __builtin__
模块覆盖此方法并围绕 __builtin__.__import__
方法进行包装。或者您可以使用 imp
模块中的 NullImporter
钩子。捕获异常并在 except
块中模拟您的模块/类。
指向相关文档的指针:
Accessing Import internals with the imp Module
我希望这有帮助。强烈建议您进入 Python 编程更神秘的领域,并且 a) 扎实理解您真正想要实现的目标和 b) 彻底理解其含义很重要。
我知道这是一个相当古老的问题,但我发现自己最近几次回到它,并想分享一个简洁的解决方案。
import sys
from unittest import mock
def mock_module_import(module):
"""Source: https://stackoverflow.com/a/63584866/3972558"""
def _outer_wrapper(func):
def _inner_wrapper(*args, **kwargs):
orig = sys.modules.get(module) # get the original module, if present
sys.modules[module] = mock.MagicMock() # patch it
try:
return func(*args, **kwargs)
finally:
if orig is not None: # if the module was installed, restore patch
sys.modules[module] = orig
else: # if the module never existed, remove the key
del sys.modules[module]
return _inner_wrapper
return _outer_wrapper
它的工作原理是临时修补 sys.modules
中模块的密钥,然后在调用修饰函数后恢复原始模块。这可以用于测试环境中可能未安装软件包的场景,或者更复杂的场景,其中修补模块可能实际上执行一些它自己的内部猴子修补(这是我所面临的情况)。
这是一个使用示例:
@mock_module_import("some_module")
def test_foo():
# use something that relies upon "some_module" here
assert True
我今天发现自己面临着类似的问题,我决定以不同的方式解决它。您可以简单地将模拟模块添加到 sys.path
中,而不是在 Python 的导入机制上进行破解,让 Python 更喜欢它而不是原始模块。
在子目录中创建替换模块,例如: mkdir -p test/mocked-lib ${EDITOR} test/mocked-lib/B.py 在导入 A 之前,将此目录插入到 sys.path。我正在使用 pytest,所以在我的 test/conftest.py 中,我简单地完成了: import os.path import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), “模拟库”))
现在,当测试套件运行时,mocked-lib
子目录被添加到 sys.path
中,并且 import A
使用来自 mocked-lib
的 B
。
Mock
不会修补某些魔法属性 (__%s__
),例如__name__
。sys.modules['B'] = None
,但它似乎不起作用。mock
,然后调用mock.Mock()