Pytest 模拟补丁 - 如何排除故障?

Pytest mocker patch - how to troubleshoot?

我遇到了一个我认为是使用模拟补丁的常见问题,因为我无法找出正确的事情 来打补丁。

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

  1. 关于如何解决以下示例中的特定问题的想法
  2. 可能最重要的是 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),它会执行两个操作:

  1. sys.modules
  2. 中搜索pyarrow模块
  3. 它将搜索结果绑定到本地范围内的名称 (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 == '.'