Skip to content

Commit c961278

Browse files
tirkarthicjw296
authored andcommitted
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
1 parent 03acba6 commit c961278

File tree

3 files changed

+86
-1
lines changed

3 files changed

+86
-1
lines changed

Lib/unittest/mock.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,14 +804,48 @@ def _format_mock_failure_message(self, args, kwargs, action='call'):
804804
return message % (action, expected_string, actual_string)
805805

806806

807+
def _get_call_signature_from_name(self, name):
808+
"""
809+
* If call objects are asserted against a method/function like obj.meth1
810+
then there could be no name for the call object to lookup. Hence just
811+
return the spec_signature of the method/function being asserted against.
812+
* If the name is not empty then remove () and split by '.' to get
813+
list of names to iterate through the children until a potential
814+
match is found. A child mock is created only during attribute access
815+
so if we get a _SpecState then no attributes of the spec were accessed
816+
and can be safely exited.
817+
"""
818+
if not name:
819+
return self._spec_signature
820+
821+
sig = None
822+
names = name.replace('()', '').split('.')
823+
children = self._mock_children
824+
825+
for name in names:
826+
child = children.get(name)
827+
if child is None or isinstance(child, _SpecState):
828+
break
829+
else:
830+
children = child._mock_children
831+
sig = child._spec_signature
832+
833+
return sig
834+
835+
807836
def _call_matcher(self, _call):
808837
"""
809838
Given a call (or simply an (args, kwargs) tuple), return a
810839
comparison key suitable for matching with other calls.
811840
This is a best effort method which relies on the spec's signature,
812841
if available, or falls back on the arguments themselves.
813842
"""
814-
sig = self._spec_signature
843+
844+
if isinstance(_call, tuple) and len(_call) > 2:
845+
sig = self._get_call_signature_from_name(_call[0])
846+
else:
847+
sig = self._spec_signature
848+
815849
if sig is not None:
816850
if len(_call) == 2:
817851
name = ''

Lib/unittest/test/testmock/testmock.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,54 @@ def test_assert_has_calls(self):
13471347
)
13481348

13491349

1350+
def test_assert_has_calls_nested_spec(self):
1351+
class Something:
1352+
1353+
def __init__(self): pass
1354+
def meth(self, a, b, c, d=None): pass
1355+
1356+
class Foo:
1357+
1358+
def __init__(self, a): pass
1359+
def meth1(self, a, b): pass
1360+
1361+
mock_class = create_autospec(Something)
1362+
1363+
for m in [mock_class, mock_class()]:
1364+
m.meth(1, 2, 3, d=1)
1365+
m.assert_has_calls([call.meth(1, 2, 3, d=1)])
1366+
m.assert_has_calls([call.meth(1, 2, 3, 1)])
1367+
1368+
mock_class.reset_mock()
1369+
1370+
for m in [mock_class, mock_class()]:
1371+
self.assertRaises(AssertionError, m.assert_has_calls, [call.Foo()])
1372+
m.Foo(1).meth1(1, 2)
1373+
m.assert_has_calls([call.Foo(1), call.Foo(1).meth1(1, 2)])
1374+
m.Foo.assert_has_calls([call(1), call().meth1(1, 2)])
1375+
1376+
mock_class.reset_mock()
1377+
1378+
invalid_calls = [call.meth(1),
1379+
call.non_existent(1),
1380+
call.Foo().non_existent(1),
1381+
call.Foo().meth(1, 2, 3, 4)]
1382+
1383+
for kall in invalid_calls:
1384+
self.assertRaises(AssertionError,
1385+
mock_class.assert_has_calls,
1386+
[kall]
1387+
)
1388+
1389+
1390+
def test_assert_has_calls_nested_without_spec(self):
1391+
m = MagicMock()
1392+
m().foo().bar().baz()
1393+
m.one().two().three()
1394+
calls = call.one().two().three().call_list()
1395+
m.assert_has_calls(calls)
1396+
1397+
13501398
def test_assert_has_calls_with_function_spec(self):
13511399
def f(a, b, c, d=None): pass
13521400

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Ensure method signature is used instead of constructor signature of a class
2+
while asserting mock object against method calls. Patch by Karthikeyan
3+
Singaravelan.

0 commit comments

Comments
 (0)