[Python-checkins] bpo-36871: Ensure method signature is used when asserting mock calls to a method (GH13261)

Chris Withers webhook-mailer at python.org
Thu Aug 29 02:09:06 EDT 2019


https://github.com/python/cpython/commit/c96127821ebda50760e788b1213975a0d5bea37f
commit: c96127821ebda50760e788b1213975a0d5bea37f
branch: master
author: Xtreak <tir.karthi at gmail.com>
committer: Chris Withers <chris at withers.org>
date: 2019-08-29T07:09:01+01:00
summary:

bpo-36871: Ensure method signature is used when asserting mock calls to a method (GH13261)

* Fix call_matcher for mock when using methods

* Add NEWS entry

* Use None check and convert doctest to unittest

* Use better name for mock in tests. Handle _SpecState when the attribute was not accessed and add tests.

* Use reset_mock instead of reinitialization. Change inner class constructor signature for check

* Reword comment regarding call object lookup logic

files:
A Misc/NEWS.d/next/Library/2019-05-12-12-58-37.bpo-36871.6xiEHZ.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 298b41e0d7e4..5846eeb404d2 100644
--- a/Lib/unittest/mock.py
+++ b/Lib/unittest/mock.py
@@ -804,6 +804,35 @@ def _format_mock_failure_message(self, args, kwargs, action='call'):
         return message % (action, expected_string, actual_string)
 
 
+    def _get_call_signature_from_name(self, name):
+        """
+        * If call objects are asserted against a method/function like obj.meth1
+        then there could be no name for the call object to lookup. Hence just
+        return the spec_signature of the method/function being asserted against.
+        * If the name is not empty then remove () and split by '.' to get
+        list of names to iterate through the children until a potential
+        match is found. A child mock is created only during attribute access
+        so if we get a _SpecState then no attributes of the spec were accessed
+        and can be safely exited.
+        """
+        if not name:
+            return self._spec_signature
+
+        sig = None
+        names = name.replace('()', '').split('.')
+        children = self._mock_children
+
+        for name in names:
+            child = children.get(name)
+            if child is None or isinstance(child, _SpecState):
+                break
+            else:
+                children = child._mock_children
+                sig = child._spec_signature
+
+        return sig
+
+
     def _call_matcher(self, _call):
         """
         Given a call (or simply an (args, kwargs) tuple), return a
@@ -811,7 +840,12 @@ def _call_matcher(self, _call):
         This is a best effort method which relies on the spec's signature,
         if available, or falls back on the arguments themselves.
         """
-        sig = self._spec_signature
+
+        if isinstance(_call, tuple) and len(_call) > 2:
+            sig = self._get_call_signature_from_name(_call[0])
+        else:
+            sig = self._spec_signature
+
         if sig is not None:
             if len(_call) == 2:
                 name = ''
diff --git a/Lib/unittest/test/testmock/testmock.py b/Lib/unittest/test/testmock/testmock.py
index 69b34e9c4fb7..265eb1bba56c 100644
--- a/Lib/unittest/test/testmock/testmock.py
+++ b/Lib/unittest/test/testmock/testmock.py
@@ -1347,6 +1347,54 @@ def test_assert_has_calls(self):
                         )
 
 
+    def test_assert_has_calls_nested_spec(self):
+        class Something:
+
+            def __init__(self): pass
+            def meth(self, a, b, c, d=None): pass
+
+            class Foo:
+
+                def __init__(self, a): pass
+                def meth1(self, a, b): pass
+
+        mock_class = create_autospec(Something)
+
+        for m in [mock_class, mock_class()]:
+            m.meth(1, 2, 3, d=1)
+            m.assert_has_calls([call.meth(1, 2, 3, d=1)])
+            m.assert_has_calls([call.meth(1, 2, 3, 1)])
+
+        mock_class.reset_mock()
+
+        for m in [mock_class, mock_class()]:
+            self.assertRaises(AssertionError, m.assert_has_calls, [call.Foo()])
+            m.Foo(1).meth1(1, 2)
+            m.assert_has_calls([call.Foo(1), call.Foo(1).meth1(1, 2)])
+            m.Foo.assert_has_calls([call(1), call().meth1(1, 2)])
+
+        mock_class.reset_mock()
+
+        invalid_calls = [call.meth(1),
+                         call.non_existent(1),
+                         call.Foo().non_existent(1),
+                         call.Foo().meth(1, 2, 3, 4)]
+
+        for kall in invalid_calls:
+            self.assertRaises(AssertionError,
+                              mock_class.assert_has_calls,
+                              [kall]
+            )
+
+
+    def test_assert_has_calls_nested_without_spec(self):
+        m = MagicMock()
+        m().foo().bar().baz()
+        m.one().two().three()
+        calls = call.one().two().three().call_list()
+        m.assert_has_calls(calls)
+
+
     def test_assert_has_calls_with_function_spec(self):
         def f(a, b, c, d=None): pass
 
diff --git a/Misc/NEWS.d/next/Library/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst b/Misc/NEWS.d/next/Library/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst
new file mode 100644
index 000000000000..218795f203be
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-05-12-12-58-37.bpo-36871.6xiEHZ.rst
@@ -0,0 +1,3 @@
+Ensure method signature is used instead of constructor signature of a class
+while asserting mock object against method calls. Patch by Karthikeyan
+Singaravelan.



More information about the Python-checkins mailing list