Python: 模拟修补一个模块,无论它是从哪里导入的
Python: mock patch a module wherever it is imported from
我需要确保 运行 单元测试不会触发调用繁重的外部世界函数,比如这个函数:
# bigbad.py
def request(param):
return 'I searched the whole Internet for "{}"'.format(param)
多个模块使用这个函数(bigbad.request)并且它们以不同的方式导入它(在现实生活中它也可能从外部库导入)。比如说,有两个模块 a 和 b,其中 b 依赖于 a 并且都使用函数:
# a.py, from...import
from bigbad import request
def routine_a():
return request('a')
# b.py, imports directly
import a
import bigbad
def routine_b():
resp_a = a.routine_a()
return 'resp_a: {}, resp_b=request(resp_a): {}'.format(resp_a, bigbad.request(resp_a))
有没有办法确保 bigbad.request 永远不会被调用?此代码仅模拟其中一个导入:
# test_b.py
import unittest
from unittest import mock
import b
with mock.patch('bigbad.request') as mock_request:
mock_request.return_value = 'mocked'
print(b.routine_b())
显然我可以重构 b 并更改导入,但是这样我不能保证在未来的开发过程中有人不会破坏这个规定。我认为测试应该测试行为而不是实现细节。
import bigbad
bigbad.request = # some dummy function
只要在执行 from bigbad import request
的任何模块 run/imported 之前 运行 秒,它就可以工作。也就是说,只要他们运行之后,他们就会收到dummy函数。
# a.py, from...import
from bigbad import request
为确保永远不会调用原始 request
,您必须修补所有导入引用的地方:
import mock
with mock.patch('a.request', return_value='mocked') as mock_request:
...
这很乏味,所以如果可能的话不要在代码中使用 from bigbad import request
,而是使用 import bigbad; bigbad.request
。
另一个解决方案:如果可能,更改bigbad.py
:
# bigbad.py
def _request(param):
return 'I searched the whole Internet for "{}"'.format(param)
def request(param):
return _request(param)
然后,即使某些代码可以做到 from bigbad import request
,您也可以做到 with mock.patch('bigbad._request', return_value='mocked') as mock_request:
。
对于未来遇到这个问题的任何人,我编写了一个函数来修补给定符号的所有导入。
此函数 returns 每次导入给定符号(整个模块、特定函数或任何其他对象)的修补程序列表。然后,这些修补程序可以 started/stopped 在您的测试夹具的 setup/teardown 区域中(有关示例,请参见文档字符串)。
工作原理:
- 遍历
sys.modules
中每个当前可见的模块
- 如果模块名称以
match_prefix
(可选)开头且不包含skip_substring
(可选),遍历模块中的每个局部
- 如果本地是
target_symbol
,为它创建一个修补程序,本地到它导入的模块
我建议使用 skip_substring='test'
这样的参数,这样您就不会修补测试套件导入的内容。
from typing import Any, Optional
import unittest.mock as mock
import sys
def patch_all_symbol_imports(
target_symbol: Any, match_prefix: Optional[str] = None,
skip_substring: Optional[str] = None
):
"""
Iterate through every visible module (in sys.modules) that starts with
`match_prefix` to find imports of `target_symbol` and return a list
of patchers for each import.
This is helpful when you want to patch a module, function, or object
everywhere in your project's code, even when it is imported with an alias.
Example:
::
import datetime
# Setup
patchers = patch_all_symbol_imports(datetime, 'my_project.', 'test')
for patcher in patchers:
mock_dt = patcher.start()
# Do stuff with the mock
# Teardown
for patcher in patchers:
patcher.stop()
:param target_symbol: the symbol to search for imports of (may be a module,
a function, or some other object)
:param match_prefix: if not None, only search for imports in
modules that begin with this string
:param skip_substring: if not None, skip any module that contains this
substring (e.g. 'test' to skip unit test modules)
:return: a list of patchers for each import of the target symbol
"""
patchers = []
# Iterate through all currently imported modules
# Make a copy in case it changes
for module in list(sys.modules.values()):
name_matches = (
match_prefix is None
or module.__name__.startswith(match_prefix)
)
should_skip = (
skip_substring is not None and skip_substring in module.__name__
)
if not name_matches or should_skip:
continue
# Iterate through this module's locals
# Again, make a copy
for local_name, local in list(module.__dict__.items()):
if local is target_symbol:
# Patch this symbol local to the module
patchers.append(mock.patch(
f'{module.__name__}.{local_name}', autospec=True
))
return patchers
具体这道题,可以使用如下代码:
from bigbad import request
patchers = patch_all_symbol_imports(request, skip_substring='test')
for patcher in patchers:
mock_request = patcher.start()
mock_request.return_value = 'mocked'
print(b.routine_b())
for patcher in patchers:
patcher.stop()
我需要确保 运行 单元测试不会触发调用繁重的外部世界函数,比如这个函数:
# bigbad.py
def request(param):
return 'I searched the whole Internet for "{}"'.format(param)
多个模块使用这个函数(bigbad.request)并且它们以不同的方式导入它(在现实生活中它也可能从外部库导入)。比如说,有两个模块 a 和 b,其中 b 依赖于 a 并且都使用函数:
# a.py, from...import
from bigbad import request
def routine_a():
return request('a')
# b.py, imports directly
import a
import bigbad
def routine_b():
resp_a = a.routine_a()
return 'resp_a: {}, resp_b=request(resp_a): {}'.format(resp_a, bigbad.request(resp_a))
有没有办法确保 bigbad.request 永远不会被调用?此代码仅模拟其中一个导入:
# test_b.py
import unittest
from unittest import mock
import b
with mock.patch('bigbad.request') as mock_request:
mock_request.return_value = 'mocked'
print(b.routine_b())
显然我可以重构 b 并更改导入,但是这样我不能保证在未来的开发过程中有人不会破坏这个规定。我认为测试应该测试行为而不是实现细节。
import bigbad
bigbad.request = # some dummy function
只要在执行 from bigbad import request
的任何模块 run/imported 之前 运行 秒,它就可以工作。也就是说,只要他们运行之后,他们就会收到dummy函数。
# a.py, from...import
from bigbad import request
为确保永远不会调用原始 request
,您必须修补所有导入引用的地方:
import mock
with mock.patch('a.request', return_value='mocked') as mock_request:
...
这很乏味,所以如果可能的话不要在代码中使用 from bigbad import request
,而是使用 import bigbad; bigbad.request
。
另一个解决方案:如果可能,更改bigbad.py
:
# bigbad.py
def _request(param):
return 'I searched the whole Internet for "{}"'.format(param)
def request(param):
return _request(param)
然后,即使某些代码可以做到 from bigbad import request
,您也可以做到 with mock.patch('bigbad._request', return_value='mocked') as mock_request:
。
对于未来遇到这个问题的任何人,我编写了一个函数来修补给定符号的所有导入。
此函数 returns 每次导入给定符号(整个模块、特定函数或任何其他对象)的修补程序列表。然后,这些修补程序可以 started/stopped 在您的测试夹具的 setup/teardown 区域中(有关示例,请参见文档字符串)。
工作原理:
- 遍历
sys.modules
中每个当前可见的模块
- 如果模块名称以
match_prefix
(可选)开头且不包含skip_substring
(可选),遍历模块中的每个局部 - 如果本地是
target_symbol
,为它创建一个修补程序,本地到它导入的模块
我建议使用 skip_substring='test'
这样的参数,这样您就不会修补测试套件导入的内容。
from typing import Any, Optional
import unittest.mock as mock
import sys
def patch_all_symbol_imports(
target_symbol: Any, match_prefix: Optional[str] = None,
skip_substring: Optional[str] = None
):
"""
Iterate through every visible module (in sys.modules) that starts with
`match_prefix` to find imports of `target_symbol` and return a list
of patchers for each import.
This is helpful when you want to patch a module, function, or object
everywhere in your project's code, even when it is imported with an alias.
Example:
::
import datetime
# Setup
patchers = patch_all_symbol_imports(datetime, 'my_project.', 'test')
for patcher in patchers:
mock_dt = patcher.start()
# Do stuff with the mock
# Teardown
for patcher in patchers:
patcher.stop()
:param target_symbol: the symbol to search for imports of (may be a module,
a function, or some other object)
:param match_prefix: if not None, only search for imports in
modules that begin with this string
:param skip_substring: if not None, skip any module that contains this
substring (e.g. 'test' to skip unit test modules)
:return: a list of patchers for each import of the target symbol
"""
patchers = []
# Iterate through all currently imported modules
# Make a copy in case it changes
for module in list(sys.modules.values()):
name_matches = (
match_prefix is None
or module.__name__.startswith(match_prefix)
)
should_skip = (
skip_substring is not None and skip_substring in module.__name__
)
if not name_matches or should_skip:
continue
# Iterate through this module's locals
# Again, make a copy
for local_name, local in list(module.__dict__.items()):
if local is target_symbol:
# Patch this symbol local to the module
patchers.append(mock.patch(
f'{module.__name__}.{local_name}', autospec=True
))
return patchers
具体这道题,可以使用如下代码:
from bigbad import request
patchers = patch_all_symbol_imports(request, skip_substring='test')
for patcher in patchers:
mock_request = patcher.start()
mock_request.return_value = 'mocked'
print(b.routine_b())
for patcher in patchers:
patcher.stop()