bpo-35330: Don't call the wrapped object if `side_effect` is set (GH10973)

* 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.
This commit is contained in:
Mario Corchero 2018-12-08 11:25:02 +00:00 committed by Chris Withers
parent 3cf74384b5
commit f05df0a4b6
3 changed files with 136 additions and 11 deletions

View File

@ -1027,28 +1027,27 @@ class CallableMixin(Base):
break
seen.add(_new_parent_id)
ret_val = DEFAULT
effect = self.side_effect
if effect is not None:
if _is_exception(effect):
raise effect
if not _callable(effect):
elif not _callable(effect):
result = next(effect)
if _is_exception(result):
raise result
if result is DEFAULT:
result = self.return_value
else:
result = effect(*args, **kwargs)
if result is not DEFAULT:
return result
ret_val = effect(*args, **kwargs)
if self._mock_return_value is not DEFAULT:
return self.return_value
if (self._mock_wraps is not None and
self._mock_return_value is DEFAULT):
if self._mock_wraps is not None:
return self._mock_wraps(*args, **kwargs)
if ret_val is DEFAULT:
ret_val = self.return_value
return ret_val
return self.return_value

View File

@ -558,6 +558,16 @@ class MockTest(unittest.TestCase):
real.assert_called_with(1, 2, fish=3)
def test_wraps_prevents_automatic_creation_of_mocks(self):
class Real(object):
pass
real = Real()
mock = Mock(wraps=real)
self.assertRaises(AttributeError, lambda: mock.new_attr())
def test_wraps_call_with_nondefault_return_value(self):
real = Mock()
@ -584,6 +594,118 @@ class MockTest(unittest.TestCase):
self.assertEqual(result, Real.attribute.frog())
def test_customize_wrapped_object_with_side_effect_iterable_with_default(self):
class Real(object):
def method(self):
return sentinel.ORIGINAL_VALUE
real = Real()
mock = Mock(wraps=real)
mock.method.side_effect = [sentinel.VALUE1, DEFAULT]
self.assertEqual(mock.method(), sentinel.VALUE1)
self.assertEqual(mock.method(), sentinel.ORIGINAL_VALUE)
self.assertRaises(StopIteration, mock.method)
def test_customize_wrapped_object_with_side_effect_iterable(self):
class Real(object):
def method(self):
raise NotImplementedError()
real = Real()
mock = Mock(wraps=real)
mock.method.side_effect = [sentinel.VALUE1, sentinel.VALUE2]
self.assertEqual(mock.method(), sentinel.VALUE1)
self.assertEqual(mock.method(), sentinel.VALUE2)
self.assertRaises(StopIteration, mock.method)
def test_customize_wrapped_object_with_side_effect_exception(self):
class Real(object):
def method(self):
raise NotImplementedError()
real = Real()
mock = Mock(wraps=real)
mock.method.side_effect = RuntimeError
self.assertRaises(RuntimeError, mock.method)
def test_customize_wrapped_object_with_side_effect_function(self):
class Real(object):
def method(self):
raise NotImplementedError()
def side_effect():
return sentinel.VALUE
real = Real()
mock = Mock(wraps=real)
mock.method.side_effect = side_effect
self.assertEqual(mock.method(), sentinel.VALUE)
def test_customize_wrapped_object_with_return_value(self):
class Real(object):
def method(self):
raise NotImplementedError()
real = Real()
mock = Mock(wraps=real)
mock.method.return_value = sentinel.VALUE
self.assertEqual(mock.method(), sentinel.VALUE)
def test_customize_wrapped_object_with_return_value_and_side_effect(self):
# side_effect should always take precedence over return_value.
class Real(object):
def method(self):
raise NotImplementedError()
real = Real()
mock = Mock(wraps=real)
mock.method.side_effect = [sentinel.VALUE1, sentinel.VALUE2]
mock.method.return_value = sentinel.WRONG_VALUE
self.assertEqual(mock.method(), sentinel.VALUE1)
self.assertEqual(mock.method(), sentinel.VALUE2)
self.assertRaises(StopIteration, mock.method)
def test_customize_wrapped_object_with_return_value_and_side_effect2(self):
# side_effect can return DEFAULT to default to return_value
class Real(object):
def method(self):
raise NotImplementedError()
real = Real()
mock = Mock(wraps=real)
mock.method.side_effect = lambda: DEFAULT
mock.method.return_value = sentinel.VALUE
self.assertEqual(mock.method(), sentinel.VALUE)
def test_customize_wrapped_object_with_return_value_and_side_effect_default(self):
class Real(object):
def method(self):
raise NotImplementedError()
real = Real()
mock = Mock(wraps=real)
mock.method.side_effect = [sentinel.VALUE1, DEFAULT]
mock.method.return_value = sentinel.RETURN
self.assertEqual(mock.method(), sentinel.VALUE1)
self.assertEqual(mock.method(), sentinel.RETURN)
self.assertRaises(StopIteration, mock.method)
def test_exceptional_side_effect(self):
mock = Mock(side_effect=AttributeError)
self.assertRaises(AttributeError, mock)

View File

@ -0,0 +1,4 @@
When a :class:`Mock` instance was used to wrap an object, if `side_effect`
is used in one of the mocks of it methods, don't call the original
implementation and return the result of using the side effect the same way
that it is done with return_value.