Pytest 模拟补丁 - 如何排除故障?
Pytest mocker patch - how to troubleshoot?
我遇到了一个我认为是使用模拟补丁的常见问题,因为我无法找出正确的事情 来打补丁。
我有两个问题希望得到帮助。
- 关于如何解决以下示例中的特定问题的想法
- 可能最重要的是 pro-tips/pointers/thoughts/suggestions 关于如何最好地解决 "which thing do I patch" 问题。我遇到的问题是,如果不完全了解补丁的工作原理,我真的不知道我应该寻找什么,结果发现自己在玩猜谜游戏。
使用 pyarrow
的一个例子目前让我很痛苦:
mymodule.py
import pyarrow
class HdfsSearch:
def __init__(self):
self.fs = self._connect()
def _connect(self) -> object:
return pyarrow.hdfs.connect(driver="libhdfs")
def search(self, path: str):
return self.fs.ls(path=path)
test_module.py
import pyarrow
import pytest
from mymodule import HdfsSearch
@pytest.fixture()
def hdfs_connection_fixture(mocker):
mocker.patch("pyarrow.hdfs.connect")
yield HdfsSearch()
def test_hdfs_connection(hdfs_connection_fixture):
pyarrow.hdfs.connect.assert_called_once() # <-- succeeds
def test_hdfs_search(hdfs_connection_fixture):
hdfs_connection_fixture.search(".")
pyarrow.hdfs.HadoopFileSystem.ls.assert_called_once() # <-- fails
pytest 输出:
$ python -m pytest --verbose test_module.py
=========================================================================================================== test session starts ============================================================================================================
platform linux -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- /home/bbaur/miniconda3/envs/dev/bin/python
cachedir: .pytest_cache
rootdir: /home/user1/work/app
plugins: cov-2.7.1, mock-1.10.4
collected 2 items
test_module.py::test_hdfs_connection PASSED [ 50%]
test_module.py::test_hdfs_search FAILED [100%]
================================================================================================================= FAILURES =================================================================================================================
_____________________________________________________________________________________________________________ test_hdfs_search _____________________________________________________________________________________________________________
hdfs_connection_fixture = <mymodule.HdfsSearch object at 0x7fdb4ec2a610>
def test_hdfs_search(hdfs_connection_fixture):
hdfs_connection_fixture.search(".")
> pyarrow.hdfs.HadoopFileSystem.ls.assert_called_once()
E AttributeError: 'function' object has no attribute 'assert_called_once'
test_module.py:16: AttributeError
您没有在 Mock 对象上调用断言,这是正确的断言:
hdfs_connection_fixture.fs.ls.assert_called_once()
解释:
当您访问 Mock 对象中的任何属性时,它将 return 另一个 Mock 对象。
自从您修补 "pyarrow.hdfs.connect"
之后,您已将其替换为 Mock,我们称其为 Mock A。您的 _connect
方法将 return Mock A,您将分配它至 self.fs
.
现在让我们分解一下调用 self.fs.ls
.
时 search
方法中发生的事情
self.fs
return 是你的 Mock A 对象,然后 .ls
将 return 一个不同的 Mock 对象,我们称它为 Mock B。在这个 Mock B 对象中您正在拨打电话 (path=path)
.
在您的断言中,您正在尝试访问 pyarrow.hdfs.HadoopFileSystem
,但它从未被修补过。您需要对 Mock B 对象执行断言,该对象位于 hdfs_connection_fixture.fs.ls
修补什么
如果您将 mymodule.py
中的导入更改为此 from pyarrow.hdfs import connect
,您的补丁将停止工作。
这是为什么?
当你修补某些东西时,你正在改变 name
指向的东西,而不是实际的对象。
您当前的补丁正在修补名称 pyarrow.hdfs.connect
并且在 mymodule 中您使用相同的名称 pyarrow.hdfs.connect
所以一切都很好。
但是,如果您使用 from pyarrow.hdfs import connect
,mymodule 将导入真正的 pyarrow.hdfs.connect
并为其创建一个名为 mymodule.connect
的引用。
因此,当您在 mymodule
中调用 connect
时,您正在访问未修补的名称 mymodule.connect
。
这就是您在使用 from import 时需要修补 mymodule.connect
的原因。
我建议在进行此类修补时使用 from x import y
。它使您试图模拟的内容更加明确,并且补丁将仅限于该模块,这可以防止不可预见的副作用。
来源,Python 文档中的此部分:Where to patch
要了解补丁在 python 中的工作原理,让我们首先了解 import 语句。
当我们在模块中使用 import pyarrow
时(在本例中为 mymodule.py),它会执行两个操作:
- 在
sys.modules
中搜索pyarrow
模块
- 它将搜索结果绑定到本地范围内的名称 (
pyarrow
)。
通过做类似的事情:pyarrow = sys.modules['pyarrow']
注意:python 中的 import
语句不执行代码。 import 语句将名称引入本地范围。只有当 python 在 sys.modules
中找不到模块时,代码的执行才会作为副作用发生
因此,要修补在 mymodule.py 中导入的 pyarrow ,我们需要修补出现在 pyarrow
的本地范围内的名称=48=]
patch('mymodule.pyarrow', autospec=True)
test_module.py
import pytest
from mock import Mock, sentinel
from pyarrow import hdfs
from mymodule import HdfsSearch
class TestHdfsSearch(object):
@pytest.fixture(autouse=True, scope='function')
def setup(self, mocker):
self.hdfs_mock = Mock(name='HadoopFileSystem', spec=hdfs.HadoopFileSystem)
self.connect_mock = mocker.patch("mymodule.pyarrow.hdfs.connect", return_value=self.hdfs_mock)
def test_initialize_HdfsSearch_should_connect_pyarrow_hdfs_file_system(self):
HdfsSearch()
self.connect_mock.assert_called_once_with(driver="libhdfs")
def test_initialize_HdfsSearch_should_set_pyarrow_hdfs_as_file_system(self):
hdfs_search = HdfsSearch()
assert self.hdfs_mock == hdfs_search.fs
def test_search_should_retrieve_directory_contents(self):
hdfs_search = HdfsSearch()
self.hdfs_mock.ls.return_value = sentinel.contents
result = hdfs_search.search(".")
self.hdfs_mock.ls.assert_called_once_with(path=".")
assert sentinel.contents == result
使用上下文管理器修补内置函数
def test_patch_built_ins():
with patch('os.curdir') as curdir_mock: # curdir_mock lives only inside with block. Doesn't lives outside
assert curdir_mock == os.curdir
assert os.curdir == '.'
我遇到了一个我认为是使用模拟补丁的常见问题,因为我无法找出正确的事情 来打补丁。
我有两个问题希望得到帮助。
- 关于如何解决以下示例中的特定问题的想法
- 可能最重要的是 pro-tips/pointers/thoughts/suggestions 关于如何最好地解决 "which thing do I patch" 问题。我遇到的问题是,如果不完全了解补丁的工作原理,我真的不知道我应该寻找什么,结果发现自己在玩猜谜游戏。
使用 pyarrow
的一个例子目前让我很痛苦:
mymodule.py
import pyarrow
class HdfsSearch:
def __init__(self):
self.fs = self._connect()
def _connect(self) -> object:
return pyarrow.hdfs.connect(driver="libhdfs")
def search(self, path: str):
return self.fs.ls(path=path)
test_module.py
import pyarrow
import pytest
from mymodule import HdfsSearch
@pytest.fixture()
def hdfs_connection_fixture(mocker):
mocker.patch("pyarrow.hdfs.connect")
yield HdfsSearch()
def test_hdfs_connection(hdfs_connection_fixture):
pyarrow.hdfs.connect.assert_called_once() # <-- succeeds
def test_hdfs_search(hdfs_connection_fixture):
hdfs_connection_fixture.search(".")
pyarrow.hdfs.HadoopFileSystem.ls.assert_called_once() # <-- fails
pytest 输出:
$ python -m pytest --verbose test_module.py
=========================================================================================================== test session starts ============================================================================================================
platform linux -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- /home/bbaur/miniconda3/envs/dev/bin/python
cachedir: .pytest_cache
rootdir: /home/user1/work/app
plugins: cov-2.7.1, mock-1.10.4
collected 2 items
test_module.py::test_hdfs_connection PASSED [ 50%]
test_module.py::test_hdfs_search FAILED [100%]
================================================================================================================= FAILURES =================================================================================================================
_____________________________________________________________________________________________________________ test_hdfs_search _____________________________________________________________________________________________________________
hdfs_connection_fixture = <mymodule.HdfsSearch object at 0x7fdb4ec2a610>
def test_hdfs_search(hdfs_connection_fixture):
hdfs_connection_fixture.search(".")
> pyarrow.hdfs.HadoopFileSystem.ls.assert_called_once()
E AttributeError: 'function' object has no attribute 'assert_called_once'
test_module.py:16: AttributeError
您没有在 Mock 对象上调用断言,这是正确的断言:
hdfs_connection_fixture.fs.ls.assert_called_once()
解释:
当您访问 Mock 对象中的任何属性时,它将 return 另一个 Mock 对象。
自从您修补 "pyarrow.hdfs.connect"
之后,您已将其替换为 Mock,我们称其为 Mock A。您的 _connect
方法将 return Mock A,您将分配它至 self.fs
.
现在让我们分解一下调用 self.fs.ls
.
search
方法中发生的事情
self.fs
return 是你的 Mock A 对象,然后 .ls
将 return 一个不同的 Mock 对象,我们称它为 Mock B。在这个 Mock B 对象中您正在拨打电话 (path=path)
.
在您的断言中,您正在尝试访问 pyarrow.hdfs.HadoopFileSystem
,但它从未被修补过。您需要对 Mock B 对象执行断言,该对象位于 hdfs_connection_fixture.fs.ls
修补什么
如果您将 mymodule.py
中的导入更改为此 from pyarrow.hdfs import connect
,您的补丁将停止工作。
这是为什么?
当你修补某些东西时,你正在改变 name
指向的东西,而不是实际的对象。
您当前的补丁正在修补名称 pyarrow.hdfs.connect
并且在 mymodule 中您使用相同的名称 pyarrow.hdfs.connect
所以一切都很好。
但是,如果您使用 from pyarrow.hdfs import connect
,mymodule 将导入真正的 pyarrow.hdfs.connect
并为其创建一个名为 mymodule.connect
的引用。
因此,当您在 mymodule
中调用 connect
时,您正在访问未修补的名称 mymodule.connect
。
这就是您在使用 from import 时需要修补 mymodule.connect
的原因。
我建议在进行此类修补时使用 from x import y
。它使您试图模拟的内容更加明确,并且补丁将仅限于该模块,这可以防止不可预见的副作用。
来源,Python 文档中的此部分:Where to patch
要了解补丁在 python 中的工作原理,让我们首先了解 import 语句。
当我们在模块中使用 import pyarrow
时(在本例中为 mymodule.py),它会执行两个操作:
- 在
sys.modules
中搜索 - 它将搜索结果绑定到本地范围内的名称 (
pyarrow
)。 通过做类似的事情:pyarrow = sys.modules['pyarrow']
pyarrow
模块
注意:python 中的 import
语句不执行代码。 import 语句将名称引入本地范围。只有当 python 在 sys.modules
因此,要修补在 mymodule.py 中导入的 pyarrow ,我们需要修补出现在 pyarrow
的本地范围内的名称=48=]
patch('mymodule.pyarrow', autospec=True)
test_module.py
import pytest
from mock import Mock, sentinel
from pyarrow import hdfs
from mymodule import HdfsSearch
class TestHdfsSearch(object):
@pytest.fixture(autouse=True, scope='function')
def setup(self, mocker):
self.hdfs_mock = Mock(name='HadoopFileSystem', spec=hdfs.HadoopFileSystem)
self.connect_mock = mocker.patch("mymodule.pyarrow.hdfs.connect", return_value=self.hdfs_mock)
def test_initialize_HdfsSearch_should_connect_pyarrow_hdfs_file_system(self):
HdfsSearch()
self.connect_mock.assert_called_once_with(driver="libhdfs")
def test_initialize_HdfsSearch_should_set_pyarrow_hdfs_as_file_system(self):
hdfs_search = HdfsSearch()
assert self.hdfs_mock == hdfs_search.fs
def test_search_should_retrieve_directory_contents(self):
hdfs_search = HdfsSearch()
self.hdfs_mock.ls.return_value = sentinel.contents
result = hdfs_search.search(".")
self.hdfs_mock.ls.assert_called_once_with(path=".")
assert sentinel.contents == result
使用上下文管理器修补内置函数
def test_patch_built_ins():
with patch('os.curdir') as curdir_mock: # curdir_mock lives only inside with block. Doesn't lives outside
assert curdir_mock == os.curdir
assert os.curdir == '.'