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)
这个测试快把我逼疯了,我想不通。
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)