Skip to content

Commit ee2c5a8

Browse files
miss-islingtonmariocj89
authored andcommitted
bpo-35330: Don't call the wrapped object if side_effect is set (GH11035)
* tests: Further validate `wraps` functionality in `unittest.mock.Mock` Add more tests to validate how `wraps` interacts with other features of mocks. * Don't call the wrapped object if `side_effect` is set When a object is wrapped using `Mock(wraps=...)`, if an user sets a `side_effect` in one of their methods, return the value of `side_effect` and don't call the original object. * Refactor what to be called on `mock_call` When a `Mock` is called, it should return looking up in the following order: `side_effect`, `return_value`, `wraps`. If any of the first two return `mock.DEFAULT`, lookup in the next option. It makes no sense to check for `wraps` returning default, as it is supposed to be the original implementation and there is nothing to fallback to. (cherry picked from commit f05df0a) Co-authored-by: Mario Corchero <[email protected]>
1 parent 783b794 commit ee2c5a8

File tree

3 files changed

+136
-11
lines changed

3 files changed

+136
-11
lines changed

Lib/unittest/mock.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,28 +1005,27 @@ def _mock_call(_mock_self, *args, **kwargs):
10051005
break
10061006
seen.add(_new_parent_id)
10071007

1008-
ret_val = DEFAULT
10091008
effect = self.side_effect
10101009
if effect is not None:
10111010
if _is_exception(effect):
10121011
raise effect
1013-
1014-
if not _callable(effect):
1012+
elif not _callable(effect):
10151013
result = next(effect)
10161014
if _is_exception(result):
10171015
raise result
1018-
if result is DEFAULT:
1019-
result = self.return_value
1016+
else:
1017+
result = effect(*args, **kwargs)
1018+
1019+
if result is not DEFAULT:
10201020
return result
10211021

1022-
ret_val = effect(*args, **kwargs)
1022+
if self._mock_return_value is not DEFAULT:
1023+
return self.return_value
10231024

1024-
if (self._mock_wraps is not None and
1025-
self._mock_return_value is DEFAULT):
1025+
if self._mock_wraps is not None:
10261026
return self._mock_wraps(*args, **kwargs)
1027-
if ret_val is DEFAULT:
1028-
ret_val = self.return_value
1029-
return ret_val
1027+
1028+
return self.return_value
10301029

10311030

10321031

Lib/unittest/test/testmock/testmock.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,16 @@ def test_wraps_calls(self):
549549
real.assert_called_with(1, 2, fish=3)
550550

551551

552+
def test_wraps_prevents_automatic_creation_of_mocks(self):
553+
class Real(object):
554+
pass
555+
556+
real = Real()
557+
mock = Mock(wraps=real)
558+
559+
self.assertRaises(AttributeError, lambda: mock.new_attr())
560+
561+
552562
def test_wraps_call_with_nondefault_return_value(self):
553563
real = Mock()
554564

@@ -575,6 +585,118 @@ class Real(object):
575585
self.assertEqual(result, Real.attribute.frog())
576586

577587

588+
def test_customize_wrapped_object_with_side_effect_iterable_with_default(self):
589+
class Real(object):
590+
def method(self):
591+
return sentinel.ORIGINAL_VALUE
592+
593+
real = Real()
594+
mock = Mock(wraps=real)
595+
mock.method.side_effect = [sentinel.VALUE1, DEFAULT]
596+
597+
self.assertEqual(mock.method(), sentinel.VALUE1)
598+
self.assertEqual(mock.method(), sentinel.ORIGINAL_VALUE)
599+
self.assertRaises(StopIteration, mock.method)
600+
601+
602+
def test_customize_wrapped_object_with_side_effect_iterable(self):
603+
class Real(object):
604+
def method(self):
605+
raise NotImplementedError()
606+
607+
real = Real()
608+
mock = Mock(wraps=real)
609+
mock.method.side_effect = [sentinel.VALUE1, sentinel.VALUE2]
610+
611+
self.assertEqual(mock.method(), sentinel.VALUE1)
612+
self.assertEqual(mock.method(), sentinel.VALUE2)
613+
self.assertRaises(StopIteration, mock.method)
614+
615+
616+
def test_customize_wrapped_object_with_side_effect_exception(self):
617+
class Real(object):
618+
def method(self):
619+
raise NotImplementedError()
620+
621+
real = Real()
622+
mock = Mock(wraps=real)
623+
mock.method.side_effect = RuntimeError
624+
625+
self.assertRaises(RuntimeError, mock.method)
626+
627+
628+
def test_customize_wrapped_object_with_side_effect_function(self):
629+
class Real(object):
630+
def method(self):
631+
raise NotImplementedError()
632+
633+
def side_effect():
634+
return sentinel.VALUE
635+
636+
real = Real()
637+
mock = Mock(wraps=real)
638+
mock.method.side_effect = side_effect
639+
640+
self.assertEqual(mock.method(), sentinel.VALUE)
641+
642+
643+
def test_customize_wrapped_object_with_return_value(self):
644+
class Real(object):
645+
def method(self):
646+
raise NotImplementedError()
647+
648+
real = Real()
649+
mock = Mock(wraps=real)
650+
mock.method.return_value = sentinel.VALUE
651+
652+
self.assertEqual(mock.method(), sentinel.VALUE)
653+
654+
655+
def test_customize_wrapped_object_with_return_value_and_side_effect(self):
656+
# side_effect should always take precedence over return_value.
657+
class Real(object):
658+
def method(self):
659+
raise NotImplementedError()
660+
661+
real = Real()
662+
mock = Mock(wraps=real)
663+
mock.method.side_effect = [sentinel.VALUE1, sentinel.VALUE2]
664+
mock.method.return_value = sentinel.WRONG_VALUE
665+
666+
self.assertEqual(mock.method(), sentinel.VALUE1)
667+
self.assertEqual(mock.method(), sentinel.VALUE2)
668+
self.assertRaises(StopIteration, mock.method)
669+
670+
671+
def test_customize_wrapped_object_with_return_value_and_side_effect2(self):
672+
# side_effect can return DEFAULT to default to return_value
673+
class Real(object):
674+
def method(self):
675+
raise NotImplementedError()
676+
677+
real = Real()
678+
mock = Mock(wraps=real)
679+
mock.method.side_effect = lambda: DEFAULT
680+
mock.method.return_value = sentinel.VALUE
681+
682+
self.assertEqual(mock.method(), sentinel.VALUE)
683+
684+
685+
def test_customize_wrapped_object_with_return_value_and_side_effect_default(self):
686+
class Real(object):
687+
def method(self):
688+
raise NotImplementedError()
689+
690+
real = Real()
691+
mock = Mock(wraps=real)
692+
mock.method.side_effect = [sentinel.VALUE1, DEFAULT]
693+
mock.method.return_value = sentinel.RETURN
694+
695+
self.assertEqual(mock.method(), sentinel.VALUE1)
696+
self.assertEqual(mock.method(), sentinel.RETURN)
697+
self.assertRaises(StopIteration, mock.method)
698+
699+
578700
def test_exceptional_side_effect(self):
579701
mock = Mock(side_effect=AttributeError)
580702
self.assertRaises(AttributeError, mock)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
When a :class:`Mock` instance was used to wrap an object, if `side_effect`
2+
is used in one of the mocks of it methods, don't call the original
3+
implementation and return the result of using the side effect the same way
4+
that it is done with return_value.

0 commit comments

Comments
 (0)