Skip to content

Commit 1a17a05

Browse files
[3.8] bpo-36871: Handle spec errors in assert_has_calls (GH-16005) (GH-16364)
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 (cherry picked from commit b5a7a4f) Co-authored-by: Samuel Freilich <[email protected]> https://bugs.python.org/issue36871 Automerge-Triggered-By: @gpshead
1 parent 081641f commit 1a17a05

File tree

4 files changed

+78
-5
lines changed

4 files changed

+78
-5
lines changed

Lib/unittest/mock.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -931,13 +931,21 @@ def assert_has_calls(self, calls, any_order=False):
931931
If `any_order` is True then the calls can be in any order, but
932932
they must all appear in `mock_calls`."""
933933
expected = [self._call_matcher(c) for c in calls]
934-
cause = expected if isinstance(expected, Exception) else None
934+
cause = next((e for e in expected if isinstance(e, Exception)), None)
935935
all_calls = _CallList(self._call_matcher(c) for c in self.mock_calls)
936936
if not any_order:
937937
if expected not in all_calls:
938+
if cause is None:
939+
problem = 'Calls not found.'
940+
else:
941+
problem = ('Error processing expected calls.\n'
942+
'Errors: {}').format(
943+
[e if isinstance(e, Exception) else None
944+
for e in expected])
938945
raise AssertionError(
939-
'Calls not found.\nExpected: %r%s'
940-
% (_CallList(calls), self._calls_repr(prefix="Actual"))
946+
f'{problem}\n'
947+
f'Expected: {_CallList(calls)}'
948+
f'{self._calls_repr(prefix="Actual").rstrip(".")}'
941949
) from cause
942950
return
943951

@@ -2220,12 +2228,20 @@ def assert_has_awaits(self, calls, any_order=False):
22202228
they must all appear in :attr:`await_args_list`.
22212229
"""
22222230
expected = [self._call_matcher(c) for c in calls]
2223-
cause = expected if isinstance(expected, Exception) else None
2231+
cause = next((e for e in expected if isinstance(e, Exception)), None)
22242232
all_awaits = _CallList(self._call_matcher(c) for c in self.await_args_list)
22252233
if not any_order:
22262234
if expected not in all_awaits:
2235+
if cause is None:
2236+
problem = 'Awaits not found.'
2237+
else:
2238+
problem = ('Error processing expected awaits.\n'
2239+
'Errors: {}').format(
2240+
[e if isinstance(e, Exception) else None
2241+
for e in expected])
22272242
raise AssertionError(
2228-
f'Awaits not found.\nExpected: {_CallList(calls)}\n'
2243+
f'{problem}\n'
2244+
f'Expected: {_CallList(calls)}\n'
22292245
f'Actual: {self.await_args_list}'
22302246
) from cause
22312247
return

Lib/unittest/test/testmock/testasync.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import inspect
3+
import re
34
import unittest
45

56
from unittest.mock import (call, AsyncMock, patch, MagicMock, create_autospec,
@@ -698,3 +699,30 @@ def test_assert_not_awaited(self):
698699
asyncio.run(self._runnable_test())
699700
with self.assertRaises(AssertionError):
700701
self.mock.assert_not_awaited()
702+
703+
def test_assert_has_awaits_not_matching_spec_error(self):
704+
async def f(x=None): pass
705+
706+
self.mock = AsyncMock(spec=f)
707+
asyncio.run(self._runnable_test(1))
708+
709+
with self.assertRaisesRegex(
710+
AssertionError,
711+
'^{}$'.format(
712+
re.escape('Awaits not found.\n'
713+
'Expected: [call()]\n'
714+
'Actual: [call(1)]'))) as cm:
715+
self.mock.assert_has_awaits([call()])
716+
self.assertIsNone(cm.exception.__cause__)
717+
718+
with self.assertRaisesRegex(
719+
AssertionError,
720+
'^{}$'.format(
721+
re.escape(
722+
'Error processing expected awaits.\n'
723+
"Errors: [None, TypeError('too many positional "
724+
"arguments')]\n"
725+
'Expected: [call(), call(1, 2)]\n'
726+
'Actual: [call(1)]'))) as cm:
727+
self.mock.assert_has_awaits([call(), call(1, 2)])
728+
self.assertIsInstance(cm.exception.__cause__, TypeError)

Lib/unittest/test/testmock/testmock.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,32 @@ def f(a, b, c, d=None): pass
14261426
mock.assert_has_calls(calls[:-1])
14271427
mock.assert_has_calls(calls[:-1], any_order=True)
14281428

1429+
def test_assert_has_calls_not_matching_spec_error(self):
1430+
def f(x=None): pass
1431+
1432+
mock = Mock(spec=f)
1433+
mock(1)
1434+
1435+
with self.assertRaisesRegex(
1436+
AssertionError,
1437+
'^{}$'.format(
1438+
re.escape('Calls not found.\n'
1439+
'Expected: [call()]\n'
1440+
'Actual: [call(1)]'))) as cm:
1441+
mock.assert_has_calls([call()])
1442+
self.assertIsNone(cm.exception.__cause__)
1443+
1444+
1445+
with self.assertRaisesRegex(
1446+
AssertionError,
1447+
'^{}$'.format(
1448+
re.escape(
1449+
'Error processing expected calls.\n'
1450+
"Errors: [None, TypeError('too many positional arguments')]\n"
1451+
"Expected: [call(), call(1, 2)]\n"
1452+
'Actual: [call(1)]'))) as cm:
1453+
mock.assert_has_calls([call(), call(1, 2)])
1454+
self.assertIsInstance(cm.exception.__cause__, TypeError)
14291455

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

0 commit comments

Comments
 (0)