Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,14 +804,48 @@ 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
comparison key suitable for matching with other calls.
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 = ''
Expand Down
48 changes: 48 additions & 0 deletions Lib/unittest/test/testmock/testmock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,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

Expand Down
Original file line number Diff line number Diff line change
@@ -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.