Skip to content

Commit ef04851

Browse files
authored
bpo-38136: Updates await_count and call_count to be different things (GH-16192)
1 parent 6f53d34 commit ef04851

File tree

5 files changed

+207
-42
lines changed

5 files changed

+207
-42
lines changed

Doc/library/unittest.mock.rst

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -514,21 +514,6 @@ the *new_callable* argument to :func:`patch`.
514514
>>> mock.call_count
515515
2
516516

517-
For :class:`AsyncMock` the :attr:`call_count` is only iterated if the function
518-
has been awaited:
519-
520-
>>> mock = AsyncMock()
521-
>>> mock() # doctest: +SKIP
522-
<coroutine object AsyncMockMixin._mock_call at ...>
523-
>>> mock.call_count
524-
0
525-
>>> async def main():
526-
... await mock()
527-
...
528-
>>> asyncio.run(main())
529-
>>> mock.call_count
530-
1
531-
532517
.. attribute:: return_value
533518

534519
Set this to configure the value returned by calling the mock:
@@ -907,19 +892,22 @@ object::
907892

908893
.. method:: assert_awaited()
909894

910-
Assert that the mock was awaited at least once.
895+
Assert that the mock was awaited at least once. Note that this is separate
896+
from the object having been called, the ``await`` keyword must be used:
911897

912898
>>> mock = AsyncMock()
913-
>>> async def main():
914-
... await mock()
899+
>>> async def main(coroutine_mock):
900+
... await coroutine_mock
915901
...
916-
>>> asyncio.run(main())
902+
>>> coroutine_mock = mock()
903+
>>> mock.called
904+
True
917905
>>> mock.assert_awaited()
918-
>>> mock_2 = AsyncMock()
919-
>>> mock_2.assert_awaited()
920906
Traceback (most recent call last):
921907
...
922908
AssertionError: Expected mock to have been awaited.
909+
>>> asyncio.run(main(coroutine_mock))
910+
>>> mock.assert_awaited()
923911

924912
.. method:: assert_awaited_once()
925913

@@ -1004,14 +992,15 @@ object::
1004992
... await mock(*args, **kwargs)
1005993
...
1006994
>>> calls = [call("foo"), call("bar")]
1007-
>>> mock.assert_has_calls(calls)
995+
>>> mock.assert_has_awaits(calls)
1008996
Traceback (most recent call last):
1009997
...
1010-
AssertionError: Calls not found.
998+
AssertionError: Awaits not found.
1011999
Expected: [call('foo'), call('bar')]
1000+
Actual: []
10121001
>>> asyncio.run(main('foo'))
10131002
>>> asyncio.run(main('bar'))
1014-
>>> mock.assert_has_calls(calls)
1003+
>>> mock.assert_has_awaits(calls)
10151004

10161005
.. method:: assert_not_awaited()
10171006

Lib/unittest/mock.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,14 +1076,20 @@ def __call__(self, /, *args, **kwargs):
10761076
# can't use self in-case a function / method we are mocking uses self
10771077
# in the signature
10781078
self._mock_check_sig(*args, **kwargs)
1079+
self._increment_mock_call(*args, **kwargs)
10791080
return self._mock_call(*args, **kwargs)
10801081

10811082

10821083
def _mock_call(self, /, *args, **kwargs):
1084+
return self._execute_mock_call(*args, **kwargs)
1085+
1086+
def _increment_mock_call(self, /, *args, **kwargs):
10831087
self.called = True
10841088
self.call_count += 1
10851089

10861090
# handle call_args
1091+
# needs to be set here so assertions on call arguments pass before
1092+
# execution in the case of awaited calls
10871093
_call = _Call((args, kwargs), two=True)
10881094
self.call_args = _call
10891095
self.call_args_list.append(_call)
@@ -1123,6 +1129,10 @@ def _mock_call(self, /, *args, **kwargs):
11231129
# follow the parental chain:
11241130
_new_parent = _new_parent._mock_new_parent
11251131

1132+
def _execute_mock_call(self, /, *args, **kwargs):
1133+
# seperate from _increment_mock_call so that awaited functions are
1134+
# executed seperately from their call
1135+
11261136
effect = self.side_effect
11271137
if effect is not None:
11281138
if _is_exception(effect):

Lib/unittest/test/testmock/testasync.py

Lines changed: 179 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import unittest
44

55
from unittest.mock import (ANY, call, AsyncMock, patch, MagicMock,
6-
create_autospec, _AwaitEvent)
6+
create_autospec, _AwaitEvent, sentinel, _CallList)
77

88

99
def tearDownModule():
@@ -595,11 +595,173 @@ class AsyncMockAssert(unittest.TestCase):
595595
def setUp(self):
596596
self.mock = AsyncMock()
597597

598-
async def _runnable_test(self, *args):
599-
if not args:
600-
await self.mock()
601-
else:
602-
await self.mock(*args)
598+
async def _runnable_test(self, *args, **kwargs):
599+
await self.mock(*args, **kwargs)
600+
601+
async def _await_coroutine(self, coroutine):
602+
return await coroutine
603+
604+
def test_assert_called_but_not_awaited(self):
605+
mock = AsyncMock(AsyncClass)
606+
with self.assertWarns(RuntimeWarning):
607+
# Will raise a warning because never awaited
608+
mock.async_method()
609+
self.assertTrue(asyncio.iscoroutinefunction(mock.async_method))
610+
mock.async_method.assert_called()
611+
mock.async_method.assert_called_once()
612+
mock.async_method.assert_called_once_with()
613+
with self.assertRaises(AssertionError):
614+
mock.assert_awaited()
615+
with self.assertRaises(AssertionError):
616+
mock.async_method.assert_awaited()
617+
618+
def test_assert_called_then_awaited(self):
619+
mock = AsyncMock(AsyncClass)
620+
mock_coroutine = mock.async_method()
621+
mock.async_method.assert_called()
622+
mock.async_method.assert_called_once()
623+
mock.async_method.assert_called_once_with()
624+
with self.assertRaises(AssertionError):
625+
mock.async_method.assert_awaited()
626+
627+
asyncio.run(self._await_coroutine(mock_coroutine))
628+
# Assert we haven't re-called the function
629+
mock.async_method.assert_called_once()
630+
mock.async_method.assert_awaited()
631+
mock.async_method.assert_awaited_once()
632+
mock.async_method.assert_awaited_once_with()
633+
634+
def test_assert_called_and_awaited_at_same_time(self):
635+
with self.assertRaises(AssertionError):
636+
self.mock.assert_awaited()
637+
638+
with self.assertRaises(AssertionError):
639+
self.mock.assert_called()
640+
641+
asyncio.run(self._runnable_test())
642+
self.mock.assert_called_once()
643+
self.mock.assert_awaited_once()
644+
645+
def test_assert_called_twice_and_awaited_once(self):
646+
mock = AsyncMock(AsyncClass)
647+
coroutine = mock.async_method()
648+
with self.assertWarns(RuntimeWarning):
649+
# The first call will be awaited so no warning there
650+
# But this call will never get awaited, so it will warn here
651+
mock.async_method()
652+
with self.assertRaises(AssertionError):
653+
mock.async_method.assert_awaited()
654+
mock.async_method.assert_called()
655+
asyncio.run(self._await_coroutine(coroutine))
656+
mock.async_method.assert_awaited()
657+
mock.async_method.assert_awaited_once()
658+
659+
def test_assert_called_once_and_awaited_twice(self):
660+
mock = AsyncMock(AsyncClass)
661+
coroutine = mock.async_method()
662+
mock.async_method.assert_called_once()
663+
asyncio.run(self._await_coroutine(coroutine))
664+
with self.assertRaises(RuntimeError):
665+
# Cannot reuse already awaited coroutine
666+
asyncio.run(self._await_coroutine(coroutine))
667+
mock.async_method.assert_awaited()
668+
669+
def test_assert_awaited_but_not_called(self):
670+
with self.assertRaises(AssertionError):
671+
self.mock.assert_awaited()
672+
with self.assertRaises(AssertionError):
673+
self.mock.assert_called()
674+
with self.assertRaises(TypeError):
675+
# You cannot await an AsyncMock, it must be a coroutine
676+
asyncio.run(self._await_coroutine(self.mock))
677+
678+
with self.assertRaises(AssertionError):
679+
self.mock.assert_awaited()
680+
with self.assertRaises(AssertionError):
681+
self.mock.assert_called()
682+
683+
def test_assert_has_calls_not_awaits(self):
684+
kalls = [call('foo')]
685+
with self.assertWarns(RuntimeWarning):
686+
# Will raise a warning because never awaited
687+
self.mock('foo')
688+
self.mock.assert_has_calls(kalls)
689+
with self.assertRaises(AssertionError):
690+
self.mock.assert_has_awaits(kalls)
691+
692+
def test_assert_has_mock_calls_on_async_mock_no_spec(self):
693+
with self.assertWarns(RuntimeWarning):
694+
# Will raise a warning because never awaited
695+
self.mock()
696+
kalls_empty = [('', (), {})]
697+
self.assertEqual(self.mock.mock_calls, kalls_empty)
698+
699+
with self.assertWarns(RuntimeWarning):
700+
# Will raise a warning because never awaited
701+
self.mock('foo')
702+
self.mock('baz')
703+
mock_kalls = ([call(), call('foo'), call('baz')])
704+
self.assertEqual(self.mock.mock_calls, mock_kalls)
705+
706+
def test_assert_has_mock_calls_on_async_mock_with_spec(self):
707+
a_class_mock = AsyncMock(AsyncClass)
708+
with self.assertWarns(RuntimeWarning):
709+
# Will raise a warning because never awaited
710+
a_class_mock.async_method()
711+
kalls_empty = [('', (), {})]
712+
self.assertEqual(a_class_mock.async_method.mock_calls, kalls_empty)
713+
self.assertEqual(a_class_mock.mock_calls, [call.async_method()])
714+
715+
with self.assertWarns(RuntimeWarning):
716+
# Will raise a warning because never awaited
717+
a_class_mock.async_method(1, 2, 3, a=4, b=5)
718+
method_kalls = [call(), call(1, 2, 3, a=4, b=5)]
719+
mock_kalls = [call.async_method(), call.async_method(1, 2, 3, a=4, b=5)]
720+
self.assertEqual(a_class_mock.async_method.mock_calls, method_kalls)
721+
self.assertEqual(a_class_mock.mock_calls, mock_kalls)
722+
723+
def test_async_method_calls_recorded(self):
724+
with self.assertWarns(RuntimeWarning):
725+
# Will raise warnings because never awaited
726+
self.mock.something(3, fish=None)
727+
self.mock.something_else.something(6, cake=sentinel.Cake)
728+
729+
self.assertEqual(self.mock.method_calls, [
730+
("something", (3,), {'fish': None}),
731+
("something_else.something", (6,), {'cake': sentinel.Cake})
732+
],
733+
"method calls not recorded correctly")
734+
self.assertEqual(self.mock.something_else.method_calls,
735+
[("something", (6,), {'cake': sentinel.Cake})],
736+
"method calls not recorded correctly")
737+
738+
def test_async_arg_lists(self):
739+
def assert_attrs(mock):
740+
names = ('call_args_list', 'method_calls', 'mock_calls')
741+
for name in names:
742+
attr = getattr(mock, name)
743+
self.assertIsInstance(attr, _CallList)
744+
self.assertIsInstance(attr, list)
745+
self.assertEqual(attr, [])
746+
747+
assert_attrs(self.mock)
748+
with self.assertWarns(RuntimeWarning):
749+
# Will raise warnings because never awaited
750+
self.mock()
751+
self.mock(1, 2)
752+
self.mock(a=3)
753+
754+
self.mock.reset_mock()
755+
assert_attrs(self.mock)
756+
757+
a_mock = AsyncMock(AsyncClass)
758+
with self.assertWarns(RuntimeWarning):
759+
# Will raise warnings because never awaited
760+
a_mock.async_method()
761+
a_mock.async_method(1, a=3)
762+
763+
a_mock.reset_mock()
764+
assert_attrs(a_mock)
603765

604766
def test_assert_awaited(self):
605767
with self.assertRaises(AssertionError):
@@ -645,20 +807,20 @@ def test_assert_awaited_once_with(self):
645807

646808
def test_assert_any_wait(self):
647809
with self.assertRaises(AssertionError):
648-
self.mock.assert_any_await('NormalFoo')
810+
self.mock.assert_any_await('foo')
649811

650-
asyncio.run(self._runnable_test('foo'))
812+
asyncio.run(self._runnable_test('baz'))
651813
with self.assertRaises(AssertionError):
652-
self.mock.assert_any_await('NormalFoo')
814+
self.mock.assert_any_await('foo')
653815

654-
asyncio.run(self._runnable_test('NormalFoo'))
655-
self.mock.assert_any_await('NormalFoo')
816+
asyncio.run(self._runnable_test('foo'))
817+
self.mock.assert_any_await('foo')
656818

657819
asyncio.run(self._runnable_test('SomethingElse'))
658-
self.mock.assert_any_await('NormalFoo')
820+
self.mock.assert_any_await('foo')
659821

660822
def test_assert_has_awaits_no_order(self):
661-
calls = [call('NormalFoo'), call('baz')]
823+
calls = [call('foo'), call('baz')]
662824

663825
with self.assertRaises(AssertionError) as cm:
664826
self.mock.assert_has_awaits(calls)
@@ -668,7 +830,7 @@ def test_assert_has_awaits_no_order(self):
668830
with self.assertRaises(AssertionError):
669831
self.mock.assert_has_awaits(calls)
670832

671-
asyncio.run(self._runnable_test('NormalFoo'))
833+
asyncio.run(self._runnable_test('foo'))
672834
with self.assertRaises(AssertionError):
673835
self.mock.assert_has_awaits(calls)
674836

@@ -703,19 +865,19 @@ async def _custom_mock_runnable_test(*args):
703865
mock_with_spec.assert_any_await(ANY, 1)
704866

705867
def test_assert_has_awaits_ordered(self):
706-
calls = [call('NormalFoo'), call('baz')]
868+
calls = [call('foo'), call('baz')]
707869
with self.assertRaises(AssertionError):
708870
self.mock.assert_has_awaits(calls, any_order=True)
709871

710872
asyncio.run(self._runnable_test('baz'))
711873
with self.assertRaises(AssertionError):
712874
self.mock.assert_has_awaits(calls, any_order=True)
713875

714-
asyncio.run(self._runnable_test('foo'))
876+
asyncio.run(self._runnable_test('bamf'))
715877
with self.assertRaises(AssertionError):
716878
self.mock.assert_has_awaits(calls, any_order=True)
717879

718-
asyncio.run(self._runnable_test('NormalFoo'))
880+
asyncio.run(self._runnable_test('foo'))
719881
self.mock.assert_has_awaits(calls, any_order=True)
720882

721883
asyncio.run(self._runnable_test('qux'))

Lib/unittest/test/testmock/testmock.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,7 @@ def test():
850850
def test_setting_call(self):
851851
mock = Mock()
852852
def __call__(self, a):
853+
self._increment_mock_call(a)
853854
return self._mock_call(a)
854855

855856
type(mock).__call__ = __call__
@@ -2025,7 +2026,7 @@ def trace(frame, event, arg): # pragma: no cover
20252026
)
20262027

20272028
mocks = [
2028-
Mock, MagicMock, NonCallableMock, NonCallableMagicMock
2029+
Mock, MagicMock, NonCallableMock, NonCallableMagicMock, AsyncMock
20292030
]
20302031

20312032
for mock in mocks:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Changes AsyncMock call count and await count to be two different counters.
2+
Now await count only counts when a coroutine has been awaited, not when it
3+
has been called, and vice-versa. Update the documentation around this.

0 commit comments

Comments
 (0)