Skip to content

Commit

Permalink
bpo-36871: Handle spec errors in assert_has_calls (GH-16005)
Browse files Browse the repository at this point in the history
The fix in PR 13261 handled the underlying issue about the spec for specific methods not being applied correctly, but it didn't fix the issue that was causing the misleading error message.

The code currently grabs a list of responses from _call_matcher (which may include exceptions). But it doesn't reach inside the list when checking if the result is an exception. This results in a misleading error message when one of the provided calls does not match the spec.


https://bugs.python.org/issue36871



Automerge-Triggered-By: @gpshead
  • Loading branch information
sfreilich authored and miss-islington committed Sep 24, 2019
1 parent bb6bf7d commit b5a7a4f
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 5 deletions.
26 changes: 21 additions & 5 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -926,13 +926,21 @@ def assert_has_calls(self, calls, any_order=False):
If `any_order` is True then the calls can be in any order, but
they must all appear in `mock_calls`."""
expected = [self._call_matcher(c) for c in calls]
cause = expected if isinstance(expected, Exception) else None
cause = next((e for e in expected if isinstance(e, Exception)), None)
all_calls = _CallList(self._call_matcher(c) for c in self.mock_calls)
if not any_order:
if expected not in all_calls:
if cause is None:
problem = 'Calls not found.'
else:
problem = ('Error processing expected calls.\n'
'Errors: {}').format(
[e if isinstance(e, Exception) else None
for e in expected])
raise AssertionError(
'Calls not found.\nExpected: %r%s'
% (_CallList(calls), self._calls_repr(prefix="Actual"))
f'{problem}\n'
f'Expected: {_CallList(calls)}\n'
f'Actual: {self._calls_repr(prefix="Actual")}'
) from cause
return

Expand Down Expand Up @@ -2244,12 +2252,20 @@ def assert_has_awaits(self, calls, any_order=False):
they must all appear in :attr:`await_args_list`.
"""
expected = [self._call_matcher(c) for c in calls]
cause = expected if isinstance(expected, Exception) else None
cause = next((e for e in expected if isinstance(e, Exception)), None)
all_awaits = _CallList(self._call_matcher(c) for c in self.await_args_list)
if not any_order:
if expected not in all_awaits:
if cause is None:
problem = 'Awaits not found.'
else:
problem = ('Error processing expected awaits.\n'
'Errors: {}').format(
[e if isinstance(e, Exception) else None
for e in expected])
raise AssertionError(
f'Awaits not found.\nExpected: {_CallList(calls)}\n'
f'{problem}\n'
f'Expected: {_CallList(calls)}\n'
f'Actual: {self.await_args_list}'
) from cause
return
Expand Down
21 changes: 21 additions & 0 deletions Lib/unittest/test/testmock/testasync.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import inspect
import re
import unittest

from unittest.mock import (ANY, call, AsyncMock, patch, MagicMock,
Expand Down Expand Up @@ -889,3 +890,23 @@ def test_assert_not_awaited(self):
asyncio.run(self._runnable_test())
with self.assertRaises(AssertionError):
self.mock.assert_not_awaited()

def test_assert_has_awaits_not_matching_spec_error(self):
async def f(): pass

mock = AsyncMock(spec=f)

with self.assertRaisesRegex(
AssertionError,
re.escape('Awaits not found.\nExpected:')) as cm:
mock.assert_has_awaits([call()])
self.assertIsNone(cm.exception.__cause__)

with self.assertRaisesRegex(
AssertionError,
re.escape('Error processing expected awaits.\n'
"Errors: [None, TypeError('too many positional "
"arguments')]\n"
'Expected:')) as cm:
mock.assert_has_awaits([call(), call('wrong')])
self.assertIsInstance(cm.exception.__cause__, TypeError)
19 changes: 19 additions & 0 deletions Lib/unittest/test/testmock/testmock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,25 @@ def f(a, b, c, d=None): pass
mock.assert_has_calls(calls[:-1])
mock.assert_has_calls(calls[:-1], any_order=True)

def test_assert_has_calls_not_matching_spec_error(self):
def f(): pass

mock = Mock(spec=f)

with self.assertRaisesRegex(
AssertionError,
re.escape('Calls not found.\nExpected:')) as cm:
mock.assert_has_calls([call()])
self.assertIsNone(cm.exception.__cause__)

with self.assertRaisesRegex(
AssertionError,
re.escape('Error processing expected calls.\n'
"Errors: [None, TypeError('too many positional "
"arguments')]\n"
'Expected:')) as cm:
mock.assert_has_calls([call(), call('wrong')])
self.assertIsInstance(cm.exception.__cause__, TypeError)

def test_assert_any_call(self):
mock = Mock()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Improve error handling for the assert_has_calls and assert_has_awaits methods of
mocks. Fixed a bug where any errors encountered while binding the expected calls
to the mock's spec were silently swallowed, leading to misleading error output.

0 comments on commit b5a7a4f

Please sign in to comment.