From: Xtreak Date: Thu, 29 Aug 2019 06:09:01 +0000 (+0530) Subject: bpo-36871: Ensure method signature is used when asserting mock calls to a method... X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=c96127821ebda50760e788b1213975a0d5bea37f;p=python 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 --- diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 298b41e0d7..5846eeb404 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -804,6 +804,35 @@ class NonCallableMock(Base): 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 @@ class NonCallableMock(Base): 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 69b34e9c4f..265eb1bba5 100644 --- a/Lib/unittest/test/testmock/testmock.py +++ b/Lib/unittest/test/testmock/testmock.py @@ -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 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 0000000000..218795f203 --- /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.