Pytest mocker.patch 正在返回 NonCallableMagicMock

Pytest mocker.patch is returning NonCallableMagicMock

这个测试快把我逼疯了,我想不通。

mocker.patch 在我的实际测试中返回 MagicMock(如预期)。但是,当它调用模块并且我想要修补的 class 被模拟时,它返回 NonCallableMagicMock,而不是 MagicMock。因此,当我执行 assert_called_with 时,它会失败并引发错误,因为两者不同。

我是不是打错补丁了?我确保修补 class ,它位于使用它的模块的命名空间内,而不是 class 所在的实际模块的命名空间。返回 NonCallableMagicMock 的事实让我相信我正在修补正确的目标。

所以如果我打补丁是正确的,那么为什么我会收到这个错误?我如何断言该函数是使用 MyQuery 的实例作为参数调用的?

我的代码结构如下:

.
├── main.py
├── src
│   ├── handler
│   │   └── my_query_handler.py
│   ├── query
│   │   └── my_query.py
│   └── repo
│       └── my_repo.py
└── tests
    └── handler
        └── test_my_query_handler.py

所有文件的代码如下:

my_query_handler.py

from src.query.my_query import MyQuery
from src.repo.my_repo import MyRepo

class MyQueryHandler:

    def handle(self, repo: MyRepo):
        
        query = MyQuery(value_one="Hello", value_two="World")

        result = repo.exec(query=query)

        return result

my_query.py

class MyQuery:

    _value_one: str
    _value_two: str

    def __init__(self, value_one: str, value_two: str):

        self._value_one = value_one
        self._value_two = value_two

    def get_query(self) -> str:
        return f"{self._value_one} {self._value_two}"

my_repo.py

from src.query.my_query import MyQuery

class MyRepo:

    def exec(self, query: MyQuery):

        return query.get_query()

test_my_query_handler.py

import pytest

from src.repo.my_repo import MyRepo
from src.handler.my_query_handler import MyQueryHandler
from src.handler.my_query_handler import MyQuery

from unittest.mock import MagicMock

class TestMyQueryHandler:

    @pytest.fixture
    def mock_query(self, mocker):
        namespace = f"{MyQueryHandler.__module__}.{MyQuery.__name__}"
        return mocker.patch(namespace, autospec=True)

    def test_my_query_handler(self, mock_query):

        expected_value = 'Hello World'

        mock_repo = MagicMock(spec=MyRepo)
        mock_repo.exec.return_value = expected_value

        handler = MyQueryHandler()
        result = handler.handle(mock_repo)

        mock_repo.exec.assert_called_with(query=mock_query)

        assert result == expected_value

main.py

from src.handler.my_query_handler import MyQueryHandler
from src.repo.my_repo import MyRepo

handler = MyQueryHandler()
repo = MyRepo()

print(handler.handle(repo))

当我 运行 这些测试时,从 mock_query 返回的 mock 是 MagicMock:

<MagicMock name='MyQuery' spec='MyQuery' id='4365116176'

然而,当我 运行 测试时,当模块被修补时,它创建了一个 NonCallableMagicMock

<NonCallableMagicMock name='MyQuery()' spec='MyQuery' id='4365118992'>

当我执行 assert_called_with

时会生成以下错误
_______________________________________________________ TestMyQueryHandler.test_my_query_handler _______________________________________________________

__wrapped_mock_method__ = <function NonCallableMock.assert_called_with at 0x105b407a0>, args = (<MagicMock name='mock.exec' id='4375790672'>,)
kwargs = {'query': <MagicMock name='MyQuery' spec='MyQuery' id='4391261264'>}, __tracebackhide__ = True
msg = "Expected call: exec(query=<MagicMock name='MyQuery' spec='MyQuery' id='4391261264'>)\nActual call: exec(query=<NonCal...)' spec='MyQuery' id='4391264144'>}\n  ?            +++++++++++                       ++                           + ^"
__mock_self = <MagicMock name='mock.exec' id='4375790672'>, actual_args = ()
actual_kwargs = {'query': <NonCallableMagicMock name='MyQuery()' spec='MyQuery' id='4391264144'>}
introspection = "\nKwargs:\nassert {'query': <No...'4391264144'>} == {'query': <Ma...'4391261264'>}\n  Differing items:\n  {'query': <...)' spec='MyQuery' id='4391264144'>}\n  ?            +++++++++++                       ++                           + ^"
@py_assert2 = None, @py_assert1 = False

    def assert_wrapper(
        __wrapped_mock_method__: Callable[..., Any], *args: Any, **kwargs: Any
    ) -> None:
        __tracebackhide__ = True
        try:
>           __wrapped_mock_method__(*args, **kwargs)

env/lib/python3.7/site-packages/pytest_mock/plugin.py:414: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

_mock_self = <MagicMock name='mock.exec' id='4375790672'>, args = (), kwargs = {'query': <MagicMock name='MyQuery' spec='MyQuery' id='4391261264'>}
expected = ((), {'query': <MagicMock name='MyQuery' spec='MyQuery' id='4391261264'>})
_error_message = <function NonCallableMock.assert_called_with.<locals>._error_message at 0x105bb6440>
actual = call(query=<NonCallableMagicMock name='MyQuery()' spec='MyQuery' id='4391264144'>), cause = None

    def assert_called_with(_mock_self, *args, **kwargs):
        """assert that the mock was called with the specified arguments.
    
        Raises an AssertionError if the args and keyword args passed in are
        different to the last call to the mock."""
        self = _mock_self
        if self.call_args is None:
            expected = self._format_mock_call_signature(args, kwargs)
            raise AssertionError('Expected call: %s\nNot called' % (expected,))
    
        def _error_message():
            msg = self._format_mock_failure_message(args, kwargs)
            return msg
        expected = self._call_matcher((args, kwargs))
        actual = self._call_matcher(self.call_args)
        if expected != actual:
            cause = expected if isinstance(expected, Exception) else None
>           raise AssertionError(_error_message()) from cause
E           AssertionError: Expected call: exec(query=<MagicMock name='MyQuery' spec='MyQuery' id='4391261264'>)
E           Actual call: exec(query=<NonCallableMagicMock name='MyQuery()' spec='MyQuery' id='4391264144'>)

../../../.pyenv/versions/3.7.10/lib/python3.7/unittest/mock.py:878: AssertionError

During handling of the above exception, another exception occurred:

self = <test_my_query_handler.TestMyQueryHandler object at 0x105bc9b90>, mock_query = <MagicMock name='MyQuery' spec='MyQuery' id='4391261264'>

    def test_my_query_handler(self, mock_query):
    
        expected_value = 'Hello World'
    
        mock_repo = MagicMock(spec=MyRepo)
        mock_repo.exec.return_value = expected_value
    
        handler = MyQueryHandler()
        result = handler.handle(mock_repo)
    
>       mock_repo.exec.assert_called_with(query=mock_query)
E       AssertionError: Expected call: exec(query=<MagicMock name='MyQuery' spec='MyQuery' id='4391261264'>)
E       Actual call: exec(query=<NonCallableMagicMock name='MyQuery()' spec='MyQuery' id='4391264144'>)
E       
E       pytest introspection follows:
E       
E       Kwargs:
E       assert {'query': <No...'4391264144'>} == {'query': <Ma...'4391261264'>}
E         Differing items:
E         {'query': <NonCallableMagicMock name='MyQuery()' spec='MyQuery' id='4391264144'>} != {'query': <MagicMock name='MyQuery' spec='MyQuery' id='4391261264'>}
E         Full diff:
E         - {'query': <MagicMock name='MyQuery' spec='MyQuery' id='4391261264'>}
E         ?                                                               ^^
E         + {'query': <NonCallableMagicMock name='MyQuery()' spec='MyQuery' id='4391264144'>}
E         ?            +++++++++++                       ++                           + ^

tests/handler/test_my_query_handler.py:34: AssertionError

如果您使用 autospec,并且您正在模拟 class,则模拟的行为类似于 class。您不能在 class 上调用实例方法,因为您需要一个实例,对于模拟,您可以在 class 模拟上使用 return_value

因此,要修复您的代码,您只需使用实例模拟而不是 class 模拟,或者通过调整夹具:

    @pytest.fixture
    def mock_query(self, mocker):
        namespace = f"{MyQueryHandler.__module__}.{MyQuery.__name__}"
        return mocker.patch(namespace, autospec=True).return_value

或通过修改调用者:

    result = handler.handle(mock_repo)
    mock_repo.exec.assert_called_with(query=mock_query.return_value)