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) | ||||
| 
 | ||||
| 
 | ||||
|     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 @@ def _call_matcher(self, _call): | |||
|         This is a best effort method which relies on the spec's signature, | ||||
|         if available, or falls back on the arguments themselves. | ||||
|         """ | ||||
| 
 | ||||
|         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 = '' | ||||
|  |  | |||
|  | @ -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 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