Skip to content

Commit ad0ff86

Browse files
authored
[3.12] gh-75988: Fix issues with autospec ignoring wrapped object (GH-115223) (#117119)
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 d3de3a2 commit ad0ff86

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
@@ -2782,3 +2782,123 @@ Sealing mocks
27822782
>>> mock.not_submock.attribute2 # This won't raise.
27832783

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

Lib/test/test_unittest/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()

Lib/unittest/mock.py

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

546-
if ret is DEFAULT:
546+
if ret is DEFAULT and self._mock_wraps is None:
547547
ret = self._get_child_mock(
548548
_new_parent=self, _new_name='()'
549549
)
@@ -1204,6 +1204,9 @@ def _execute_mock_call(self, /, *args, **kwargs):
12041204
if self._mock_return_value is not DEFAULT:
12051205
return self.return_value
12061206

1207+
if self._mock_delegate and self._mock_delegate.return_value is not DEFAULT:
1208+
return self.return_value
1209+
12071210
if self._mock_wraps is not None:
12081211
return self._mock_wraps(*args, **kwargs)
12091212

@@ -2754,9 +2757,12 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
27542757
if _parent is not None and not instance:
27552758
_parent._mock_children[_name] = mock
27562759

2760+
wrapped = kwargs.get('wraps')
2761+
27572762
if is_type and not instance and 'return_value' not in kwargs:
27582763
mock.return_value = create_autospec(spec, spec_set, instance=True,
2759-
_name='()', _parent=mock)
2764+
_name='()', _parent=mock,
2765+
wraps=wrapped)
27602766

27612767
for entry in dir(spec):
27622768
if _is_magic(entry):
@@ -2778,6 +2784,9 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
27782784
continue
27792785

27802786
kwargs = {'spec': original}
2787+
# Wrap child attributes also.
2788+
if wrapped and hasattr(wrapped, entry):
2789+
kwargs.update(wraps=original)
27812790
if spec_set:
27822791
kwargs = {'spec_set': original}
27832792

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)