pytest模拟补丁-如何进行故障排除?

时间:2019-09-13 18:41:50

标签: python mocking pytest pyarrow

我在使用模拟补丁程序时遇到一个普遍的问题,因为我无法弄清楚要修补的正确的东西

我有两个问题希望得到帮助。

  1. 在以下示例中,有关如何解决特定问题的思考
  2. 关于如何最好地解决“我要修补哪件事”问题的最重要的专家提示/指针/想法/建议。我遇到的问题是,在不完全了解修补程序的工作原理的情况下,我什至不知道自己应该寻找什么并发现自己正在玩猜谜游戏。

使用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

2 个答案:

答案 0 :(得分:3)

您没有在Mock对象上调用断言,这是正确的断言:

hdfs_connection_fixture.fs.ls.assert_called_once()

说明:

当您访问Mock对象中的任何属性时,它将返回另一个Mock对象。

自从修补"pyarrow.hdfs.connect"以来,您已经用Mock替换了它,我们称其为MockA。您的_connect方法将返回该Mock A,并将其分配给self.fs

现在让我们分解一下在调用searchself.fs.ls方法中发生的事情。

self.fs返回您的Mock A对象,然后.ls将返回另一个Mock对象,我们称其为MockB。在此Mock B对象中,您正在通过{{1} }。

在您的断言中,您尝试访问(path=path),但从未对其进行修补。您需要对pyarrow.hdfs.HadoopFileSystem

上的Mock B对象进行断言

要修补的内容

如果您将hdfs_connection_fixture.fs.ls中的导入更改为此mymodule.py,则补丁将停止工作。

那是为什么?

修补某些内容时,您是在更改from pyarrow.hdfs import connect指向的内容,而不是实际的对象。

您当前的补丁程序正在修补名称name,在mymodule中,您使用的名称是pyarrow.hdfs.connect,所以一切正常。

但是,如果您使用pyarrow.hdfs.connect,则mymodule将导入真实的from pyarrow.hdfs import connect并为其创建一个名为pyarrow.hdfs.connect的引用。

因此,当您在mymodule.connect内部调用connect时,您正在访问的名称mymodule未打补丁。

这就是为什么从导入使用时需要修补mymodule.connect的原因。

在进行此类修补时,我建议使用mymodule.connect。它使您要模拟的内容更加明确,并且补丁程序将仅限于该模块,从而可以防止无法预料的副作用。

来源,Python文档中的此部分:Where to patch

答案 1 :(得分:0)

要了解补丁在python中的工作原理,首先让我们了解import语句。

当我们在模块(在本例中为mymodule.py)中使用import pyarrow时,它将执行两项操作:

  1. 它在pyarrow中搜索sys.modules模块
  2. 它将搜索结果绑定到本地范围内的名称(pyarrow)。 通过执行以下操作:pyarrow = sys.modules['pyarrow']

注意:python中的import语句不执行代码。 import语句将名称引入本地范围。仅当python在sys.modules中找不到模块时,代码的执行才会产生副作用。

因此,要修补mymodule.py中导入的 pyarrow ,我们需要修补mymodule.py

本地范围内存在的pyarrow名称>
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 == '.'