Python: 是否可以包装“@patch(path)”以供重复使用? (单元测试)
Python: is it possible to wrap "@patch(path)" for re-use? (unittest)
正如文档 "Where to patch" 所说,我们需要修补查找对象的位置(而不是定义对象的位置);所以我知道不可能 - 比方说 - 为特定路径创建可重复使用的补丁
假设您有多个模块导入您想要模拟的对象
# file_a.py
from foo.goo.hoo import settings
# file_b.py
from foo.goo.hoo import settings
# file_c.py
from foo.goo.hoo import settings
我想知道是否有办法创建装饰器,例如:
@mock_settings
def test_whatever(self, settings_mock):
...
而不是这个解决方案:
@patch("some_module.file_a.settings")
def test_whatever(self, settings_mock):
...
@patch("some_module.file_b.settings")
def test_whatever(self, settings_mock):
...
@patch("some_module.file_c.settings")
def test_whatever(self, settings_mock):
...
如问题中所述,要修补一个对象,您必须修补其在要测试的模块中的引用(如果它是使用 from ...import
导入的)。
要在多个模块中修补它,您可以使用相同的 mock 修补所有这些模块,并使用该 mock。如果您事先知道要修补哪些模块,则可以这样做。如果您不事先知道它们,您必须尝试在所有加载的模块中修补该对象——这可能会有点复杂。
我将展示一个使用 pytest 和 pytest fixture 的示例,因为它更紧凑;您可以将其包装在装饰器中以便在 unittest
中使用,但这不会改变基础知识。假设我们有一个 class 需要在多个模块中模拟:
class_to_mock.py
class ClassToMock:
def foo(self, msg):
return msg
module1.py
from class_to_mock import ClassToMock
def do_something():
inst = ClassToMock()
return inst.foo("module1")
module2.py
from class_to_mock import ClassToMock
def do_something_else():
inst = ClassToMock()
return inst.foo("module2")
您现在可以编写一个 fixture 来同时模拟所有这些模块中的 class(为简单起见,此处使用 pytest-mock
):
@pytest.fixture
def mocked_class(mocker):
mocked = Mock()
for module in ('module1', 'module2'):
mocker.patch(module + '.ClassToMock', mocked)
yield mocked
这可用于测试两个模块:
def test_module1(mocked_class):
mocked_class.return_value.foo.return_value = 'mocked!'
assert module1.do_something() == 'mocked!'
def test_module2(mocked_class):
mocked_class.return_value.foo.return_value = 'mocked!'
assert module2.do_something_else() == 'mocked!'
如果你想要一个模拟所有加载模块中的class的通用版本,你可以用这样的东西替换夹具:
@pytest.fixture
def mocked_class(mocker):
mocked = Mock()
for name, module in list(sys.modules.items()):
if not inspect.ismodule(module):
continue
for cls_name, cls in module.__dict__.items():
try: # need that as inspect may raise for some modules
if inspect.isclass(cls) and cls_name == "ClassToMock":
mocker.patch(name + ".ClassToMock", mocked)
except Exception:
continue
yield mocked
这将适用于这个特定的例子——为了概括这一点,它必须考虑更多的对象类型,class 应该是可配置的,并且可能会有更多的问题——与更简单的版本相反您只需枚举要修补的模块,这将始终有效。
你可以在 unittest.setUp
中做类似的事情,方法是将模拟放在一个实例变量中,尽管这样不太优雅,因为你还负责停止模拟:
class ModulesTest(unittest.TestCase):
def setUp(self):
self.mocked_class = Mock()
self.mocks = []
for module in ('module1', 'module2'):
mocked = mock.patch(module + '.ClassToMock', self.mocked_class)
self.mocks.append(mocked)
mocked.start()
def tearDown(self):
for mocked in self.mocks:
mocked.stop()
def test_module1(self):
self.mocked_class.return_value.foo.return_value = 'mocked!'
assert module1.do_something() == 'mocked!'
你也可以用装饰器包装它,至少部分地回答你原来的问题:
def mocked_class_to_mock(f):
@wraps(f)
def _mocked_class_to_mock(*args, **kwargs):
mocked_class = Mock()
mocks = []
for module in ('module1', 'module2'):
mocked = mock.patch(module + '.ClassToMock', mocked_class)
mocks.append(mocked)
mocked.start()
kwargs['mocked_class'] = mocked_class # use a keyword arg for simplicity
f(*args, **kwargs)
for mocked in mocks:
mocked.stop()
return _mocked_class_to_mock
...
@mocked_class_to_mock
def test_module3(self, mocked_class):
mocked_class.return_value.foo.return_value = 'mocked!'
assert module3.do_something() == 'mocked!'
当然,如果需要,您可以对更通用的版本执行相同的操作。
另请注意,我跳过了使用 import ...
导入对象的更简单的情况。在这种情况下,您必须修补原始模块。在通用夹具中,您可能希望始终添加这种情况。
正如文档 "Where to patch" 所说,我们需要修补查找对象的位置(而不是定义对象的位置);所以我知道不可能 - 比方说 - 为特定路径创建可重复使用的补丁
假设您有多个模块导入您想要模拟的对象
# file_a.py
from foo.goo.hoo import settings
# file_b.py
from foo.goo.hoo import settings
# file_c.py
from foo.goo.hoo import settings
我想知道是否有办法创建装饰器,例如:
@mock_settings
def test_whatever(self, settings_mock):
...
而不是这个解决方案:
@patch("some_module.file_a.settings")
def test_whatever(self, settings_mock):
...
@patch("some_module.file_b.settings")
def test_whatever(self, settings_mock):
...
@patch("some_module.file_c.settings")
def test_whatever(self, settings_mock):
...
如问题中所述,要修补一个对象,您必须修补其在要测试的模块中的引用(如果它是使用 from ...import
导入的)。
要在多个模块中修补它,您可以使用相同的 mock 修补所有这些模块,并使用该 mock。如果您事先知道要修补哪些模块,则可以这样做。如果您不事先知道它们,您必须尝试在所有加载的模块中修补该对象——这可能会有点复杂。
我将展示一个使用 pytest 和 pytest fixture 的示例,因为它更紧凑;您可以将其包装在装饰器中以便在 unittest
中使用,但这不会改变基础知识。假设我们有一个 class 需要在多个模块中模拟:
class_to_mock.py
class ClassToMock:
def foo(self, msg):
return msg
module1.py
from class_to_mock import ClassToMock
def do_something():
inst = ClassToMock()
return inst.foo("module1")
module2.py
from class_to_mock import ClassToMock
def do_something_else():
inst = ClassToMock()
return inst.foo("module2")
您现在可以编写一个 fixture 来同时模拟所有这些模块中的 class(为简单起见,此处使用 pytest-mock
):
@pytest.fixture
def mocked_class(mocker):
mocked = Mock()
for module in ('module1', 'module2'):
mocker.patch(module + '.ClassToMock', mocked)
yield mocked
这可用于测试两个模块:
def test_module1(mocked_class):
mocked_class.return_value.foo.return_value = 'mocked!'
assert module1.do_something() == 'mocked!'
def test_module2(mocked_class):
mocked_class.return_value.foo.return_value = 'mocked!'
assert module2.do_something_else() == 'mocked!'
如果你想要一个模拟所有加载模块中的class的通用版本,你可以用这样的东西替换夹具:
@pytest.fixture
def mocked_class(mocker):
mocked = Mock()
for name, module in list(sys.modules.items()):
if not inspect.ismodule(module):
continue
for cls_name, cls in module.__dict__.items():
try: # need that as inspect may raise for some modules
if inspect.isclass(cls) and cls_name == "ClassToMock":
mocker.patch(name + ".ClassToMock", mocked)
except Exception:
continue
yield mocked
这将适用于这个特定的例子——为了概括这一点,它必须考虑更多的对象类型,class 应该是可配置的,并且可能会有更多的问题——与更简单的版本相反您只需枚举要修补的模块,这将始终有效。
你可以在 unittest.setUp
中做类似的事情,方法是将模拟放在一个实例变量中,尽管这样不太优雅,因为你还负责停止模拟:
class ModulesTest(unittest.TestCase):
def setUp(self):
self.mocked_class = Mock()
self.mocks = []
for module in ('module1', 'module2'):
mocked = mock.patch(module + '.ClassToMock', self.mocked_class)
self.mocks.append(mocked)
mocked.start()
def tearDown(self):
for mocked in self.mocks:
mocked.stop()
def test_module1(self):
self.mocked_class.return_value.foo.return_value = 'mocked!'
assert module1.do_something() == 'mocked!'
你也可以用装饰器包装它,至少部分地回答你原来的问题:
def mocked_class_to_mock(f):
@wraps(f)
def _mocked_class_to_mock(*args, **kwargs):
mocked_class = Mock()
mocks = []
for module in ('module1', 'module2'):
mocked = mock.patch(module + '.ClassToMock', mocked_class)
mocks.append(mocked)
mocked.start()
kwargs['mocked_class'] = mocked_class # use a keyword arg for simplicity
f(*args, **kwargs)
for mocked in mocks:
mocked.stop()
return _mocked_class_to_mock
...
@mocked_class_to_mock
def test_module3(self, mocked_class):
mocked_class.return_value.foo.return_value = 'mocked!'
assert module3.do_something() == 'mocked!'
当然,如果需要,您可以对更通用的版本执行相同的操作。
另请注意,我跳过了使用 import ...
导入对象的更简单的情况。在这种情况下,您必须修补原始模块。在通用夹具中,您可能希望始终添加这种情况。