从 python 中的模块中模拟所有 类

mocking all classes from a module in python

我在测试环境的 3 个不同目录中有 test_a.py、a.py 和 b.py。

b.py

class SBD():

def __init__(self):
    print("SBD created (In B)")

a.py

import b
from b import *

print("In module A")

def fun1():
  a=SBD()
  print("SBD created(In A)")

test_a.py

import unittest
import sys
from unittest.mock import Mock,patch,MagicMock

sys.path.append("../abc/")
import b as c
sys.modules['b'] = MagicMock(spec=c)

sys.path.append("../xyz/")
import a

class TestStringMethods(unittest.TestCase):

    def test_isupper(self):
        a.fun1()


if __name__ == '__main__':
    unittest.main()

在实际情况下,b.py 将有多个 类,我想模拟所有这些,所以我尝试模拟具有相同规范的模块 b。但是当我 运行 和 test_a.py 它给我一个错误,说 "SBD" 没有定义。我在这里做错了什么?

A MagicMock 实例不会像模块那样向导入机器提供相同的信息。即使使用 spec,也没有在 mock 上定义实际的 SDB 属性,因此 from b import * 找不到它。

from * 导入机制尝试两种不同的方法:

  • 它尝试访问名称__all__;如果已定义,它必须是要导入的名称字符串列表。
  • 如果未定义 __all__,则采用 __dict__ 属性的键,过滤掉以下划线开头的名称。

因为您的 b 模块没有定义 __all__ 列表,所以取而代之的是 __dict__ 键。对于针对模块指定的 MagicMock 实例,__dict__ 属性仅包含带有 _ 下划线的名称和 mock_calls 属性。 from b import * 仅导入 mock_calls:

>>> import a as c
>>> module_mock = MagicMock(spec=c)
>>> [n for n in module_mock.__dict__ if n[:1] != '_']
['method_calls']

我强烈建议反对 模拟整个模块;这样做需要您推迟导入 a,并且 脆弱 。该补丁是永久性的(测试结束时不会自动撤消)并且不支持重复运行测试或 运行 随机顺序测试。

但是如果您必须来完成这项工作,您可以先向模拟添加一个__all__属性:

sys.modules['b'] = MagicMock(spec=c, __all__=[n for n in c.__dict__ if n[:1] != '_'])

就我个人而言,我会 a) 完全避免使用 from module import * 语法。如果无论如何我都无法阻止它被使用,下一步将是在 导入后将补丁应用到 a ,循环遍历 b 模块以获得指定的替换:

# avoid manipulating sys.path if at all possible. Move that to a PYTHONPATH
# variable or install the modules properly.
import unittest
from unittest import mock

import a
import b

class TestStringMethods(unittest.TestCase):
    def setUp(self):
        # mock everything `from b import *` would import
        b_names = getattr(b, '__all__', None)
        if b_names is None:
            b_names = [n for n in b.__dict__ if n[:1] != '_']
        self.b_mocks = {}
        for name in b_names:
            orig = getattr(b, name, None)
            if orig is None:
                continue
            self.b_mocks[name] = mock.patch.object(a, name, spec=orig)
            self.b_mocks[name].start()
            self.addCleanup(self.b_mocks[name].stop)

    def test_isupper(self):
        a.fun1()

这使 sys.modules['b'] 保持不变,并处理与 from * 将加载的完全相同的名称。补丁在测试结束后再次被移除

以上测试输出:

$ python test_a.py
In module A
SBD created(In A)
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK