Skip to content

Commit 51da1dd

Browse files
authored
[3.11] gh-75988: Fix issues with autospec ignoring wrapped object (GH-115223) (#117124)
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 (cherry picked from commit 735fc2c)
1 parent 0ec8561 commit 51da1dd

File tree

4 files changed

+198
-2
lines changed

4 files changed

+198
-2
lines changed

Doc/library/unittest.mock.rst

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2780,3 +2780,123 @@ Sealing mocks
27802780
>>> mock.not_submock.attribute2 # This won't raise.
27812781

27822782
.. versionadded:: 3.7
2783+
2784+
2785+
Order of precedence of :attr:`side_effect`, :attr:`return_value` and *wraps*
2786+
----------------------------------------------------------------------------
2787+
2788+
The order of their precedence is:
2789+
2790+
1. :attr:`~Mock.side_effect`
2791+
2. :attr:`~Mock.return_value`
2792+
3. *wraps*
2793+
2794+
If all three are set, mock will return the value from :attr:`~Mock.side_effect`,
2795+
ignoring :attr:`~Mock.return_value` and the wrapped object altogether. If any
2796+
two are set, the one with the higher precedence will return the value.
2797+
Regardless of the order of which was set first, the order of precedence
2798+
remains unchanged.
2799+
2800+
>>> from unittest.mock import Mock
2801+
>>> class Order:
2802+
... @staticmethod
2803+
... def get_value():
2804+
... return "third"
2805+
...
2806+
>>> order_mock = Mock(spec=Order, wraps=Order)
2807+
>>> order_mock.get_value.side_effect = ["first"]
2808+
>>> order_mock.get_value.return_value = "second"
2809+
>>> order_mock.get_value()
2810+
'first'
2811+
2812+
As ``None`` is the default value of :attr:`~Mock.side_effect`, if you reassign
2813+
its value back to ``None``, the order of precedence will be checked between
2814+
:attr:`~Mock.return_value` and the wrapped object, ignoring
2815+
:attr:`~Mock.side_effect`.
2816+
2817+
>>> order_mock.get_value.side_effect = None
2818+
>>> order_mock.get_value()
2819+
'second'
2820+
2821+
If the value being returned by :attr:`~Mock.side_effect` is :data:`DEFAULT`,
2822+
it is ignored and the order of precedence moves to the successor to obtain the
2823+
value to return.
2824+
2825+
>>> from unittest.mock import DEFAULT
2826+
>>> order_mock.get_value.side_effect = [DEFAULT]
2827+
>>> order_mock.get_value()
2828+
'second'
2829+
2830+
When :class:`Mock` wraps an object, the default value of
2831+
:attr:`~Mock.return_value` will be :data:`DEFAULT`.
2832+
2833+
>>> order_mock = Mock(spec=Order, wraps=Order)
2834+
>>> order_mock.return_value
2835+
sentinel.DEFAULT
2836+
>>> order_mock.get_value.return_value
2837+
sentinel.DEFAULT
2838+
2839+
The order of precedence will ignore this value and it will move to the last
2840+
successor which is the wrapped object.
2841+
2842+
As the real call is being made to the wrapped object, creating an instance of
2843+
this mock will return the real instance of the class. The positional arguments,
2844+
if any, required by the wrapped object must be passed.
2845+
2846+
>>> order_mock_instance = order_mock()
2847+
>>> isinstance(order_mock_instance, Order)
2848+
True
2849+
>>> order_mock_instance.get_value()
2850+
'third'
2851+
2852+
>>> order_mock.get_value.return_value = DEFAULT
2853+
>>> order_mock.get_value()
2854+
'third'
2855+
2856+
>>> order_mock.get_value.return_value = "second"
2857+
>>> order_mock.get_value()
2858+
'second'
2859+
2860+
But if you assign ``None`` to it, this will not be ignored as it is an
2861+
explicit assignment. So, the order of precedence will not move to the wrapped
2862+
object.
2863+
2864+
>>> order_mock.get_value.return_value = None
2865+
>>> order_mock.get_value() is None
2866+
True
2867+
2868+
Even if you set all three at once when initializing the mock, the order of
2869+
precedence remains the same:
2870+
2871+
>>> order_mock = Mock(spec=Order, wraps=Order,
2872+
... **{"get_value.side_effect": ["first"],
2873+
... "get_value.return_value": "second"}
2874+
... )
2875+
...
2876+
>>> order_mock.get_value()
2877+
'first'
2878+
>>> order_mock.get_value.side_effect = None
2879+
>>> order_mock.get_value()
2880+
'second'
2881+
>>> order_mock.get_value.return_value = DEFAULT
2882+
>>> order_mock.get_value()
2883+
'third'
2884+
2885+
If :attr:`~Mock.side_effect` is exhausted, the order of precedence will not
2886+
cause a value to be obtained from the successors. Instead, ``StopIteration``
2887+
exception is raised.
2888+
2889+
>>> order_mock = Mock(spec=Order, wraps=Order)
2890+
>>> order_mock.get_value.side_effect = ["first side effect value",
2891+
... "another side effect value"]
2892+
>>> order_mock.get_value.return_value = "second"
2893+
2894+
>>> order_mock.get_value()
2895+
'first side effect value'
2896+
>>> order_mock.get_value()
2897+
'another side effect value'
2898+
2899+
>>> order_mock.get_value()
2900+
Traceback (most recent call last):
2901+
...
2902+
StopIteration

Lib/unittest/mock.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ def __get_return_value(self):
538538
if self._mock_delegate is not None:
539539
ret = self._mock_delegate.return_value
540540

541-
if ret is DEFAULT:
541+
if ret is DEFAULT and self._mock_wraps is None:
542542
ret = self._get_child_mock(
543543
_new_parent=self, _new_name='()'
544544
)
@@ -1194,6 +1194,9 @@ def _execute_mock_call(self, /, *args, **kwargs):
11941194
if self._mock_return_value is not DEFAULT:
11951195
return self.return_value
11961196

1197+
if self._mock_delegate and self._mock_delegate.return_value is not DEFAULT:
1198+
return self.return_value
1199+
11971200
if self._mock_wraps is not None:
11981201
return self._mock_wraps(*args, **kwargs)
11991202

@@ -2732,9 +2735,12 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
27322735
if _parent is not None and not instance:
27332736
_parent._mock_children[_name] = mock
27342737

2738+
wrapped = kwargs.get('wraps')
2739+
27352740
if is_type and not instance and 'return_value' not in kwargs:
27362741
mock.return_value = create_autospec(spec, spec_set, instance=True,
2737-
_name='()', _parent=mock)
2742+
_name='()', _parent=mock,
2743+
wraps=wrapped)
27382744

27392745
for entry in dir(spec):
27402746
if _is_magic(entry):
@@ -2756,6 +2762,9 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
27562762
continue
27572763

27582764
kwargs = {'spec': original}
2765+
# Wrap child attributes also.
2766+
if wrapped and hasattr(wrapped, entry):
2767+
kwargs.update(wraps=original)
27592768
if spec_set:
27602769
kwargs = {'spec_set': original}
27612770

Lib/unittest/test/testmock/testmock.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,64 @@ class B(object):
234234
with mock.patch('builtins.open', mock.mock_open()):
235235
mock.mock_open() # should still be valid with open() mocked
236236

237+
def test_create_autospec_wraps_class(self):
238+
"""Autospec a class with wraps & test if the call is passed to the
239+
wrapped object."""
240+
result = "real result"
241+
242+
class Result:
243+
def get_result(self):
244+
return result
245+
class_mock = create_autospec(spec=Result, wraps=Result)
246+
# Have to reassign the return_value to DEFAULT to return the real
247+
# result (actual instance of "Result") when the mock is called.
248+
class_mock.return_value = mock.DEFAULT
249+
self.assertEqual(class_mock().get_result(), result)
250+
# Autospec should also wrap child attributes of parent.
251+
self.assertEqual(class_mock.get_result._mock_wraps, Result.get_result)
252+
253+
def test_create_autospec_instance_wraps_class(self):
254+
"""Autospec a class instance with wraps & test if the call is passed
255+
to the wrapped object."""
256+
result = "real result"
257+
258+
class Result:
259+
@staticmethod
260+
def get_result():
261+
"""This is a static method because when the mocked instance of
262+
'Result' will call this method, it won't be able to consume
263+
'self' argument."""
264+
return result
265+
instance_mock = create_autospec(spec=Result, instance=True, wraps=Result)
266+
# Have to reassign the return_value to DEFAULT to return the real
267+
# result from "Result.get_result" when the mocked instance of "Result"
268+
# calls "get_result".
269+
instance_mock.get_result.return_value = mock.DEFAULT
270+
self.assertEqual(instance_mock.get_result(), result)
271+
# Autospec should also wrap child attributes of the instance.
272+
self.assertEqual(instance_mock.get_result._mock_wraps, Result.get_result)
273+
274+
def test_create_autospec_wraps_function_type(self):
275+
"""Autospec a function or a method with wraps & test if the call is
276+
passed to the wrapped object."""
277+
result = "real result"
278+
279+
class Result:
280+
def get_result(self):
281+
return result
282+
func_mock = create_autospec(spec=Result.get_result, wraps=Result.get_result)
283+
self.assertEqual(func_mock(Result()), result)
284+
285+
def test_explicit_return_value_even_if_mock_wraps_object(self):
286+
"""If the mock has an explicit return_value set then calls are not
287+
passed to the wrapped object and the return_value is returned instead.
288+
"""
289+
def my_func():
290+
return None
291+
func_mock = create_autospec(spec=my_func, wraps=my_func)
292+
return_value = "explicit return value"
293+
func_mock.return_value = return_value
294+
self.assertEqual(func_mock(), return_value)
237295

238296
def test_reset_mock(self):
239297
parent = Mock()
@@ -603,6 +661,14 @@ def test_wraps_calls(self):
603661
real = Mock()
604662

605663
mock = Mock(wraps=real)
664+
# If "Mock" wraps an object, just accessing its
665+
# "return_value" ("NonCallableMock.__get_return_value") should not
666+
# trigger its descriptor ("NonCallableMock.__set_return_value") so
667+
# the default "return_value" should always be "sentinel.DEFAULT".
668+
self.assertEqual(mock.return_value, DEFAULT)
669+
# It will not be "sentinel.DEFAULT" if the mock is not wrapping any
670+
# object.
671+
self.assertNotEqual(real.return_value, DEFAULT)
606672
self.assertEqual(mock(), real())
607673

608674
real.reset_mock()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed :func:`unittest.mock.create_autospec` to pass the call through to the wrapped object to return the real result.

0 commit comments

Comments
 (0)