[Python-checkins] bpo-35330: Don't call the wrapped object if `side_effect` is set (GH11035)

Chris Withers webhook-mailer at python.org
Sat Dec 8 06:47:04 EST 2018


https://github.com/python/cpython/commit/ee2c5a8e2dcf662048dbcf4e49af9b4aaf81f7d3
commit: ee2c5a8e2dcf662048dbcf4e49af9b4aaf81f7d3
branch: 3.7
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: Chris Withers <chris at withers.org>
date: 2018-12-08T11:47:01Z
summary:

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 f05df0a4b679d0acfd0b1fe6187ba2d553b37afa)

Co-authored-by: Mario Corchero <mariocj89 at gmail.com>

files:
A Misc/NEWS.d/next/Library/2018-12-06-00-43-13.bpo-35330.abB4BN.rst
M Lib/unittest/mock.py
M Lib/unittest/test/testmock/testmock.py

diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py
index 0a6535d0fc44..f641e38dc54b 100644
--- a/Lib/unittest/mock.py
+++ b/Lib/unittest/mock.py
@@ -1005,28 +1005,27 @@ def _mock_call(_mock_self, *args, **kwargs):
                 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
 
 
 
diff --git a/Lib/unittest/test/testmock/testmock.py b/Lib/unittest/test/testmock/testmock.py
index 401a01277b06..49ecbb446629 100644
--- a/Lib/unittest/test/testmock/testmock.py
+++ b/Lib/unittest/test/testmock/testmock.py
@@ -549,6 +549,16 @@ def test_wraps_calls(self):
         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()
 
@@ -575,6 +585,118 @@ class Real(object):
         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)
diff --git a/Misc/NEWS.d/next/Library/2018-12-06-00-43-13.bpo-35330.abB4BN.rst b/Misc/NEWS.d/next/Library/2018-12-06-00-43-13.bpo-35330.abB4BN.rst
new file mode 100644
index 000000000000..24d0ab84fb16
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-12-06-00-43-13.bpo-35330.abB4BN.rst
@@ -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.



More information about the Python-checkins mailing list