mirror of https://github.com/python/cpython
gh-75988: Fix issues with autospec ignoring wrapped object (#115223)
* set default return value of functional types as _mock_return_value * added test of wrapping child attributes * added backward compatibility with explicit return * added docs on the order of precedence * added test to check default return_value
This commit is contained in:
parent
7db871e4fa
commit
735fc2cbbc
|
@ -2831,3 +2831,123 @@ Sealing mocks
|
|||
>>> mock.not_submock.attribute2 # This won't raise.
|
||||
|
||||
.. versionadded:: 3.7
|
||||
|
||||
|
||||
Order of precedence of :attr:`side_effect`, :attr:`return_value` and *wraps*
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
The order of their precedence is:
|
||||
|
||||
1. :attr:`~Mock.side_effect`
|
||||
2. :attr:`~Mock.return_value`
|
||||
3. *wraps*
|
||||
|
||||
If all three are set, mock will return the value from :attr:`~Mock.side_effect`,
|
||||
ignoring :attr:`~Mock.return_value` and the wrapped object altogether. If any
|
||||
two are set, the one with the higher precedence will return the value.
|
||||
Regardless of the order of which was set first, the order of precedence
|
||||
remains unchanged.
|
||||
|
||||
>>> from unittest.mock import Mock
|
||||
>>> class Order:
|
||||
... @staticmethod
|
||||
... def get_value():
|
||||
... return "third"
|
||||
...
|
||||
>>> order_mock = Mock(spec=Order, wraps=Order)
|
||||
>>> order_mock.get_value.side_effect = ["first"]
|
||||
>>> order_mock.get_value.return_value = "second"
|
||||
>>> order_mock.get_value()
|
||||
'first'
|
||||
|
||||
As ``None`` is the default value of :attr:`~Mock.side_effect`, if you reassign
|
||||
its value back to ``None``, the order of precedence will be checked between
|
||||
:attr:`~Mock.return_value` and the wrapped object, ignoring
|
||||
:attr:`~Mock.side_effect`.
|
||||
|
||||
>>> order_mock.get_value.side_effect = None
|
||||
>>> order_mock.get_value()
|
||||
'second'
|
||||
|
||||
If the value being returned by :attr:`~Mock.side_effect` is :data:`DEFAULT`,
|
||||
it is ignored and the order of precedence moves to the successor to obtain the
|
||||
value to return.
|
||||
|
||||
>>> from unittest.mock import DEFAULT
|
||||
>>> order_mock.get_value.side_effect = [DEFAULT]
|
||||
>>> order_mock.get_value()
|
||||
'second'
|
||||
|
||||
When :class:`Mock` wraps an object, the default value of
|
||||
:attr:`~Mock.return_value` will be :data:`DEFAULT`.
|
||||
|
||||
>>> order_mock = Mock(spec=Order, wraps=Order)
|
||||
>>> order_mock.return_value
|
||||
sentinel.DEFAULT
|
||||
>>> order_mock.get_value.return_value
|
||||
sentinel.DEFAULT
|
||||
|
||||
The order of precedence will ignore this value and it will move to the last
|
||||
successor which is the wrapped object.
|
||||
|
||||
As the real call is being made to the wrapped object, creating an instance of
|
||||
this mock will return the real instance of the class. The positional arguments,
|
||||
if any, required by the wrapped object must be passed.
|
||||
|
||||
>>> order_mock_instance = order_mock()
|
||||
>>> isinstance(order_mock_instance, Order)
|
||||
True
|
||||
>>> order_mock_instance.get_value()
|
||||
'third'
|
||||
|
||||
>>> order_mock.get_value.return_value = DEFAULT
|
||||
>>> order_mock.get_value()
|
||||
'third'
|
||||
|
||||
>>> order_mock.get_value.return_value = "second"
|
||||
>>> order_mock.get_value()
|
||||
'second'
|
||||
|
||||
But if you assign ``None`` to it, this will not be ignored as it is an
|
||||
explicit assignment. So, the order of precedence will not move to the wrapped
|
||||
object.
|
||||
|
||||
>>> order_mock.get_value.return_value = None
|
||||
>>> order_mock.get_value() is None
|
||||
True
|
||||
|
||||
Even if you set all three at once when initializing the mock, the order of
|
||||
precedence remains the same:
|
||||
|
||||
>>> order_mock = Mock(spec=Order, wraps=Order,
|
||||
... **{"get_value.side_effect": ["first"],
|
||||
... "get_value.return_value": "second"}
|
||||
... )
|
||||
...
|
||||
>>> order_mock.get_value()
|
||||
'first'
|
||||
>>> order_mock.get_value.side_effect = None
|
||||
>>> order_mock.get_value()
|
||||
'second'
|
||||
>>> order_mock.get_value.return_value = DEFAULT
|
||||
>>> order_mock.get_value()
|
||||
'third'
|
||||
|
||||
If :attr:`~Mock.side_effect` is exhausted, the order of precedence will not
|
||||
cause a value to be obtained from the successors. Instead, ``StopIteration``
|
||||
exception is raised.
|
||||
|
||||
>>> order_mock = Mock(spec=Order, wraps=Order)
|
||||
>>> order_mock.get_value.side_effect = ["first side effect value",
|
||||
... "another side effect value"]
|
||||
>>> order_mock.get_value.return_value = "second"
|
||||
|
||||
>>> order_mock.get_value()
|
||||
'first side effect value'
|
||||
>>> order_mock.get_value()
|
||||
'another side effect value'
|
||||
|
||||
>>> order_mock.get_value()
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
StopIteration
|
||||
|
|
|
@ -245,6 +245,65 @@ class MockTest(unittest.TestCase):
|
|||
with mock.patch('builtins.open', mock.mock_open()):
|
||||
mock.mock_open() # should still be valid with open() mocked
|
||||
|
||||
def test_create_autospec_wraps_class(self):
|
||||
"""Autospec a class with wraps & test if the call is passed to the
|
||||
wrapped object."""
|
||||
result = "real result"
|
||||
|
||||
class Result:
|
||||
def get_result(self):
|
||||
return result
|
||||
class_mock = create_autospec(spec=Result, wraps=Result)
|
||||
# Have to reassign the return_value to DEFAULT to return the real
|
||||
# result (actual instance of "Result") when the mock is called.
|
||||
class_mock.return_value = mock.DEFAULT
|
||||
self.assertEqual(class_mock().get_result(), result)
|
||||
# Autospec should also wrap child attributes of parent.
|
||||
self.assertEqual(class_mock.get_result._mock_wraps, Result.get_result)
|
||||
|
||||
def test_create_autospec_instance_wraps_class(self):
|
||||
"""Autospec a class instance with wraps & test if the call is passed
|
||||
to the wrapped object."""
|
||||
result = "real result"
|
||||
|
||||
class Result:
|
||||
@staticmethod
|
||||
def get_result():
|
||||
"""This is a static method because when the mocked instance of
|
||||
'Result' will call this method, it won't be able to consume
|
||||
'self' argument."""
|
||||
return result
|
||||
instance_mock = create_autospec(spec=Result, instance=True, wraps=Result)
|
||||
# Have to reassign the return_value to DEFAULT to return the real
|
||||
# result from "Result.get_result" when the mocked instance of "Result"
|
||||
# calls "get_result".
|
||||
instance_mock.get_result.return_value = mock.DEFAULT
|
||||
self.assertEqual(instance_mock.get_result(), result)
|
||||
# Autospec should also wrap child attributes of the instance.
|
||||
self.assertEqual(instance_mock.get_result._mock_wraps, Result.get_result)
|
||||
|
||||
def test_create_autospec_wraps_function_type(self):
|
||||
"""Autospec a function or a method with wraps & test if the call is
|
||||
passed to the wrapped object."""
|
||||
result = "real result"
|
||||
|
||||
class Result:
|
||||
def get_result(self):
|
||||
return result
|
||||
func_mock = create_autospec(spec=Result.get_result, wraps=Result.get_result)
|
||||
self.assertEqual(func_mock(Result()), result)
|
||||
|
||||
def test_explicit_return_value_even_if_mock_wraps_object(self):
|
||||
"""If the mock has an explicit return_value set then calls are not
|
||||
passed to the wrapped object and the return_value is returned instead.
|
||||
"""
|
||||
def my_func():
|
||||
return None
|
||||
func_mock = create_autospec(spec=my_func, wraps=my_func)
|
||||
return_value = "explicit return value"
|
||||
func_mock.return_value = return_value
|
||||
self.assertEqual(func_mock(), return_value)
|
||||
|
||||
def test_explicit_parent(self):
|
||||
parent = Mock()
|
||||
mock1 = Mock(parent=parent, return_value=None)
|
||||
|
@ -622,6 +681,14 @@ class MockTest(unittest.TestCase):
|
|||
real = Mock()
|
||||
|
||||
mock = Mock(wraps=real)
|
||||
# If "Mock" wraps an object, just accessing its
|
||||
# "return_value" ("NonCallableMock.__get_return_value") should not
|
||||
# trigger its descriptor ("NonCallableMock.__set_return_value") so
|
||||
# the default "return_value" should always be "sentinel.DEFAULT".
|
||||
self.assertEqual(mock.return_value, DEFAULT)
|
||||
# It will not be "sentinel.DEFAULT" if the mock is not wrapping any
|
||||
# object.
|
||||
self.assertNotEqual(real.return_value, DEFAULT)
|
||||
self.assertEqual(mock(), real())
|
||||
|
||||
real.reset_mock()
|
||||
|
|
|
@ -573,7 +573,7 @@ class NonCallableMock(Base):
|
|||
if self._mock_delegate is not None:
|
||||
ret = self._mock_delegate.return_value
|
||||
|
||||
if ret is DEFAULT:
|
||||
if ret is DEFAULT and self._mock_wraps is None:
|
||||
ret = self._get_child_mock(
|
||||
_new_parent=self, _new_name='()'
|
||||
)
|
||||
|
@ -1234,6 +1234,9 @@ class CallableMixin(Base):
|
|||
if self._mock_return_value is not DEFAULT:
|
||||
return self.return_value
|
||||
|
||||
if self._mock_delegate and self._mock_delegate.return_value is not DEFAULT:
|
||||
return self.return_value
|
||||
|
||||
if self._mock_wraps is not None:
|
||||
return self._mock_wraps(*args, **kwargs)
|
||||
|
||||
|
@ -2785,9 +2788,12 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
|
|||
if _parent is not None and not instance:
|
||||
_parent._mock_children[_name] = mock
|
||||
|
||||
wrapped = kwargs.get('wraps')
|
||||
|
||||
if is_type and not instance and 'return_value' not in kwargs:
|
||||
mock.return_value = create_autospec(spec, spec_set, instance=True,
|
||||
_name='()', _parent=mock)
|
||||
_name='()', _parent=mock,
|
||||
wraps=wrapped)
|
||||
|
||||
for entry in dir(spec):
|
||||
if _is_magic(entry):
|
||||
|
@ -2809,6 +2815,9 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
|
|||
continue
|
||||
|
||||
kwargs = {'spec': original}
|
||||
# Wrap child attributes also.
|
||||
if wrapped and hasattr(wrapped, entry):
|
||||
kwargs.update(wraps=original)
|
||||
if spec_set:
|
||||
kwargs = {'spec_set': original}
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Fixed :func:`unittest.mock.create_autospec` to pass the call through to the wrapped object to return the real result.
|
Loading…
Reference in New Issue