Skip to content

Commit be310e0

Browse files
miss-islingtontirkarthi
authored andcommitted
bpo-36871: Ensure method signature is used when asserting mock calls to a method (GH15577)
* 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 (cherry picked from commit c961278) Co-authored-by: Xtreak <[email protected]>
1 parent 409493d commit be310e0

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
@@ -771,14 +771,48 @@ def _format_mock_failure_message(self, args, kwargs):
771771
return message % (expected_string, actual_string)
772772

773773

774+
def _get_call_signature_from_name(self, name):
775+
"""
776+
* If call objects are asserted against a method/function like obj.meth1
777+
then there could be no name for the call object to lookup. Hence just
778+
return the spec_signature of the method/function being asserted against.
779+
* If the name is not empty then remove () and split by '.' to get
780+
list of names to iterate through the children until a potential
781+
match is found. A child mock is created only during attribute access
782+
so if we get a _SpecState then no attributes of the spec were accessed
783+
and can be safely exited.
784+
"""
785+
if not name:
786+
return self._spec_signature
787+
788+
sig = None
789+
names = name.replace('()', '').split('.')
790+
children = self._mock_children
791+
792+
for name in names:
793+
child = children.get(name)
794+
if child is None or isinstance(child, _SpecState):
795+
break
796+
else:
797+
children = child._mock_children
798+
sig = child._spec_signature
799+
800+
return sig
801+
802+
774803
def _call_matcher(self, _call):
775804
"""
776805
Given a call (or simply an (args, kwargs) tuple), return a
777806
comparison key suitable for matching with other calls.
778807
This is a best effort method which relies on the spec's signature,
779808
if available, or falls back on the arguments themselves.
780809
"""
781-
sig = self._spec_signature
810+
811+
if isinstance(_call, tuple) and len(_call) > 2:
812+
sig = self._get_call_signature_from_name(_call[0])
813+
else:
814+
sig = self._spec_signature
815+
782816
if sig is not None:
783817
if len(_call) == 2:
784818
name = ''

Lib/unittest/test/testmock/testmock.py

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

13161316

1317+
def test_assert_has_calls_nested_spec(self):
1318+
class Something:
1319+
1320+
def __init__(self): pass
1321+
def meth(self, a, b, c, d=None): pass
1322+
1323+
class Foo:
1324+
1325+
def __init__(self, a): pass
1326+
def meth1(self, a, b): pass
1327+
1328+
mock_class = create_autospec(Something)
1329+
1330+
for m in [mock_class, mock_class()]:
1331+
m.meth(1, 2, 3, d=1)
1332+
m.assert_has_calls([call.meth(1, 2, 3, d=1)])
1333+
m.assert_has_calls([call.meth(1, 2, 3, 1)])
1334+
1335+
mock_class.reset_mock()
1336+
1337+
for m in [mock_class, mock_class()]:
1338+
self.assertRaises(AssertionError, m.assert_has_calls, [call.Foo()])
1339+
m.Foo(1).meth1(1, 2)
1340+
m.assert_has_calls([call.Foo(1), call.Foo(1).meth1(1, 2)])
1341+
m.Foo.assert_has_calls([call(1), call().meth1(1, 2)])
1342+
1343+
mock_class.reset_mock()
1344+
1345+
invalid_calls = [call.meth(1),
1346+
call.non_existent(1),
1347+
call.Foo().non_existent(1),
1348+
call.Foo().meth(1, 2, 3, 4)]
1349+
1350+
for kall in invalid_calls:
1351+
self.assertRaises(AssertionError,
1352+
mock_class.assert_has_calls,
1353+
[kall]
1354+
)
1355+
1356+
1357+
def test_assert_has_calls_nested_without_spec(self):
1358+
m = MagicMock()
1359+
m().foo().bar().baz()
1360+
m.one().two().three()
1361+
calls = call.one().two().three().call_list()
1362+
m.assert_has_calls(calls)
1363+
1364+
13171365
def test_assert_has_calls_with_function_spec(self):
13181366
def f(a, b, c, d=None):
13191367
pass
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)