ChatGPT解决这个技术问题 Extra ChatGPT

如何模拟导入

模块 A 在其顶部包含 import B。但是在测试条件下,我想在 Amock B(模拟 A.B)并完全避免导入 B

事实上,B 并不是故意安装在测试环境中的。

A 是被测单元。我必须导入 A 及其所有功能。 B 是我需要模拟的模块。但是,如果 A 所做的第一件事是导入 B,我如何在 A 中模拟 B 并阻止 A 导入真正的 B

(未安装 B 的原因是我使用 pypy 进行快速测试,不幸的是 B 与 pypy 尚不兼容。)

怎么可能做到这一点?


p
phoenix

您可以在导入 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

请记住,Mock 不会修补某些魔法属性 (__%s__),例如 __name__
@reclosedev - 有 Magic Mock
您如何撤消此操作以使 B 导入再次成为 ImportError?我尝试了 sys.modules['B'] = None,但它似乎不起作用。
您如何在测试结束时重置此模拟导入,以便其他单元测试文件不会受到模拟对象的影响?
为清楚起见,您应该编辑答案以实际导入 mock,然后调用 mock.Mock()
L
Luke Marlin

内置 __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,但它导入的任何内容都没有调用。
使用 Python 3.4.3 我得到 ImportError: No module named '__builtin__'
您必须导入 __builtin__
@LucasCimon,用 builtins 替换 python3 (docs.python.org/3/whatsnew/3.0.html?highlight=__builtin__) 的 __builtin__
在 Python3.7 中它会创建无限递归 :( 这行不起作用 # Store original import orig_import = import mock.patch 更改了引用。
C
Community

如何模拟导入(模拟 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)

然后代码需要一些时间才能运行,就像真正的方法一样。


D
Don

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()

A
Anthony Sottile

我意识到我在这里参加聚会有点晚了,但这里有一种使用 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')

H
Hunter_71

我找到了在 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


D
Don Question

如果您执行 import ModuleB,您实际上将内置方法 __import__ 调用为:

ModuleB = __import__('ModuleB', globals(), locals(), [], -1)

您可以通过导入 __builtin__ 模块覆盖此方法并围绕 __builtin__.__import__ 方法进行包装。或者您可以使用 imp 模块中的 NullImporter 钩子。捕获异常并在 except 块中模拟您的模块/类。

指向相关文档的指针:

docs.python.org: __import__

Accessing Import internals with the imp Module

我希望这有帮助。强烈建议您进入 Python 编程更神秘的领域,并且 a) 扎实理解您真正想要实现的目标和 b) 彻底理解其含义很重要。


J
Joop

我知道这是一个相当古老的问题,但我发现自己最近几次回到它,并想分享一个简洁的解决方案。

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

M
Michał Górny

我今天发现自己面临着类似的问题,我决定以不同的方式解决它。您可以简单地将模拟模块添加到 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-libB