mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									03acba6f1a
								
							
						
					
					
						commit
						c96127821e
					
				
					 3 changed files with 86 additions and 1 deletions
				
			
		|  | @ -804,6 +804,35 @@ def _format_mock_failure_message(self, args, kwargs, action='call'): | ||||||
|         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 @@ def _call_matcher(self, _call): | ||||||
|         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. | ||||||
|         """ |         """ | ||||||
|         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 sig is not None: | ||||||
|             if len(_call) == 2: |             if len(_call) == 2: | ||||||
|                 name = '' |                 name = '' | ||||||
|  |  | ||||||
|  | @ -1347,6 +1347,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 test_assert_has_calls_with_function_spec(self): | ||||||
|         def f(a, b, c, d=None): pass |         def f(a, b, c, d=None): pass | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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. | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Xtreak
						Xtreak