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
This commit is contained in:
Xtreak 2019-08-29 11:39:01 +05:30 committed by Chris Withers
parent 03acba6f1a
commit c96127821e
3 changed files with 86 additions and 1 deletions

View file

@ -804,6 +804,35 @@ class NonCallableMock(Base):
return message % (action, expected_string, actual_string) 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): def _call_matcher(self, _call):
""" """
Given a call (or simply an (args, kwargs) tuple), return a Given a call (or simply an (args, kwargs) tuple), return a
@ -811,7 +840,12 @@ class NonCallableMock(Base):
This is a best effort method which relies on the spec's signature, This is a best effort method which relies on the spec's signature,
if available, or falls back on the arguments themselves. if available, or falls back on the arguments themselves.
""" """
if isinstance(_call, tuple) and len(_call) > 2:
sig = self._get_call_signature_from_name(_call[0])
else:
sig = self._spec_signature sig = self._spec_signature
if sig is not None: if sig is not None:
if len(_call) == 2: if len(_call) == 2:
name = '' name = ''

View file

@ -1347,6 +1347,54 @@ class MockTest(unittest.TestCase):
) )
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 test_assert_has_calls_with_function_spec(self):
def f(a, b, c, d=None): pass def f(a, b, c, d=None): pass

View file

@ -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.