Skip to content

bpo-38136: Updates await_count and call_count to be different things #16192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 13 additions & 24 deletions Doc/library/unittest.mock.rst
Original file line number Diff line number Diff line change
Expand Up @@ -514,21 +514,6 @@ the *new_callable* argument to :func:`patch`.
>>> mock.call_count
2

For :class:`AsyncMock` the :attr:`call_count` is only iterated if the function
has been awaited:

>>> mock = AsyncMock()
>>> mock() # doctest: +SKIP
<coroutine object AsyncMockMixin._mock_call at ...>
>>> mock.call_count
0
>>> async def main():
... await mock()
...
>>> asyncio.run(main())
>>> mock.call_count
1

.. attribute:: return_value

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

.. method:: assert_awaited()

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

>>> mock = AsyncMock()
>>> async def main():
... await mock()
>>> async def main(coroutine_mock):
... await coroutine_mock
...
>>> asyncio.run(main())
>>> coroutine_mock = mock()
>>> mock.called
True
>>> mock.assert_awaited()
>>> mock_2 = AsyncMock()
>>> mock_2.assert_awaited()
Traceback (most recent call last):
...
AssertionError: Expected mock to have been awaited.
>>> asyncio.run(main(coroutine_mock))
>>> mock.assert_awaited()

.. method:: assert_awaited_once()

Expand Down Expand Up @@ -1004,14 +992,15 @@ object::
... await mock(*args, **kwargs)
...
>>> calls = [call("foo"), call("bar")]
>>> mock.assert_has_calls(calls)
>>> mock.assert_has_awaits(calls)
Traceback (most recent call last):
...
AssertionError: Calls not found.
AssertionError: Awaits not found.
Expected: [call('foo'), call('bar')]
Actual: []
>>> asyncio.run(main('foo'))
>>> asyncio.run(main('bar'))
>>> mock.assert_has_calls(calls)
>>> mock.assert_has_awaits(calls)

.. method:: assert_not_awaited()

Expand Down
10 changes: 10 additions & 0 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1072,14 +1072,20 @@ def __call__(self, /, *args, **kwargs):
# can't use self in-case a function / method we are mocking uses self
# in the signature
self._mock_check_sig(*args, **kwargs)
self._increment_mock_call(*args, **kwargs)
return self._mock_call(*args, **kwargs)


def _mock_call(self, /, *args, **kwargs):
return self._execute_mock_call(*args, **kwargs)

def _increment_mock_call(self, /, *args, **kwargs):
self.called = True
self.call_count += 1

# handle call_args
# needs to be set here so assertions on call arguments pass before
# execution in the case of awaited calls
_call = _Call((args, kwargs), two=True)
self.call_args = _call
self.call_args_list.append(_call)
Expand Down Expand Up @@ -1119,6 +1125,10 @@ def _mock_call(self, /, *args, **kwargs):
# follow the parental chain:
_new_parent = _new_parent._mock_new_parent

def _execute_mock_call(self, /, *args, **kwargs):
# seperate from _increment_mock_call so that awaited functions are
# executed seperately from their call

effect = self.side_effect
if effect is not None:
if _is_exception(effect):
Expand Down
196 changes: 179 additions & 17 deletions Lib/unittest/test/testmock/testasync.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import unittest

from unittest.mock import (ANY, call, AsyncMock, patch, MagicMock,
create_autospec, _AwaitEvent)
create_autospec, _AwaitEvent, sentinel, _CallList)


def tearDownModule():
Expand Down Expand Up @@ -528,11 +528,173 @@ class AsyncMockAssert(unittest.TestCase):
def setUp(self):
self.mock = AsyncMock()

async def _runnable_test(self, *args):
if not args:
await self.mock()
else:
await self.mock(*args)
async def _runnable_test(self, *args, **kwargs):
await self.mock(*args, **kwargs)

async def _await_coroutine(self, coroutine):
return await coroutine

def test_assert_called_but_not_awaited(self):
mock = AsyncMock(AsyncClass)
with self.assertWarns(RuntimeWarning):
# Will raise a warning because never awaited
mock.async_method()
self.assertTrue(asyncio.iscoroutinefunction(mock.async_method))
mock.async_method.assert_called()
mock.async_method.assert_called_once()
mock.async_method.assert_called_once_with()
with self.assertRaises(AssertionError):
mock.assert_awaited()
with self.assertRaises(AssertionError):
mock.async_method.assert_awaited()

def test_assert_called_then_awaited(self):
mock = AsyncMock(AsyncClass)
mock_coroutine = mock.async_method()
mock.async_method.assert_called()
mock.async_method.assert_called_once()
mock.async_method.assert_called_once_with()
with self.assertRaises(AssertionError):
mock.async_method.assert_awaited()

asyncio.run(self._await_coroutine(mock_coroutine))
# Assert we haven't re-called the function
mock.async_method.assert_called_once()
mock.async_method.assert_awaited()
mock.async_method.assert_awaited_once()
mock.async_method.assert_awaited_once_with()

def test_assert_called_and_awaited_at_same_time(self):
with self.assertRaises(AssertionError):
self.mock.assert_awaited()

with self.assertRaises(AssertionError):
self.mock.assert_called()

asyncio.run(self._runnable_test())
self.mock.assert_called_once()
self.mock.assert_awaited_once()

def test_assert_called_twice_and_awaited_once(self):
mock = AsyncMock(AsyncClass)
coroutine = mock.async_method()
with self.assertWarns(RuntimeWarning):
# The first call will be awaited so no warning there
# But this call will never get awaited, so it will warn here
mock.async_method()
with self.assertRaises(AssertionError):
mock.async_method.assert_awaited()
mock.async_method.assert_called()
asyncio.run(self._await_coroutine(coroutine))
mock.async_method.assert_awaited()
mock.async_method.assert_awaited_once()

def test_assert_called_once_and_awaited_twice(self):
mock = AsyncMock(AsyncClass)
coroutine = mock.async_method()
mock.async_method.assert_called_once()
asyncio.run(self._await_coroutine(coroutine))
with self.assertRaises(RuntimeError):
# Cannot reuse already awaited coroutine
asyncio.run(self._await_coroutine(coroutine))
mock.async_method.assert_awaited()

def test_assert_awaited_but_not_called(self):
with self.assertRaises(AssertionError):
self.mock.assert_awaited()
with self.assertRaises(AssertionError):
self.mock.assert_called()
with self.assertRaises(TypeError):
# You cannot await an AsyncMock, it must be a coroutine
asyncio.run(self._await_coroutine(self.mock))

with self.assertRaises(AssertionError):
self.mock.assert_awaited()
with self.assertRaises(AssertionError):
self.mock.assert_called()

def test_assert_has_calls_not_awaits(self):
kalls = [call('foo')]
with self.assertWarns(RuntimeWarning):
# Will raise a warning because never awaited
self.mock('foo')
self.mock.assert_has_calls(kalls)
with self.assertRaises(AssertionError):
self.mock.assert_has_awaits(kalls)

def test_assert_has_mock_calls_on_async_mock_no_spec(self):
with self.assertWarns(RuntimeWarning):
# Will raise a warning because never awaited
self.mock()
kalls_empty = [('', (), {})]
self.assertEqual(self.mock.mock_calls, kalls_empty)

with self.assertWarns(RuntimeWarning):
# Will raise a warning because never awaited
self.mock('foo')
self.mock('baz')
mock_kalls = ([call(), call('foo'), call('baz')])
self.assertEqual(self.mock.mock_calls, mock_kalls)

def test_assert_has_mock_calls_on_async_mock_with_spec(self):
a_class_mock = AsyncMock(AsyncClass)
with self.assertWarns(RuntimeWarning):
# Will raise a warning because never awaited
a_class_mock.async_method()
kalls_empty = [('', (), {})]
self.assertEqual(a_class_mock.async_method.mock_calls, kalls_empty)
self.assertEqual(a_class_mock.mock_calls, [call.async_method()])

with self.assertWarns(RuntimeWarning):
# Will raise a warning because never awaited
a_class_mock.async_method(1, 2, 3, a=4, b=5)
method_kalls = [call(), call(1, 2, 3, a=4, b=5)]
mock_kalls = [call.async_method(), call.async_method(1, 2, 3, a=4, b=5)]
self.assertEqual(a_class_mock.async_method.mock_calls, method_kalls)
self.assertEqual(a_class_mock.mock_calls, mock_kalls)

def test_async_method_calls_recorded(self):
with self.assertWarns(RuntimeWarning):
# Will raise warnings because never awaited
self.mock.something(3, fish=None)
self.mock.something_else.something(6, cake=sentinel.Cake)

self.assertEqual(self.mock.method_calls, [
("something", (3,), {'fish': None}),
("something_else.something", (6,), {'cake': sentinel.Cake})
],
"method calls not recorded correctly")
self.assertEqual(self.mock.something_else.method_calls,
[("something", (6,), {'cake': sentinel.Cake})],
"method calls not recorded correctly")

def test_async_arg_lists(self):
def assert_attrs(mock):
names = ('call_args_list', 'method_calls', 'mock_calls')
for name in names:
attr = getattr(mock, name)
self.assertIsInstance(attr, _CallList)
self.assertIsInstance(attr, list)
self.assertEqual(attr, [])

assert_attrs(self.mock)
with self.assertWarns(RuntimeWarning):
# Will raise warnings because never awaited
self.mock()
self.mock(1, 2)
self.mock(a=3)

self.mock.reset_mock()
assert_attrs(self.mock)

a_mock = AsyncMock(AsyncClass)
with self.assertWarns(RuntimeWarning):
# Will raise warnings because never awaited
a_mock.async_method()
a_mock.async_method(1, a=3)

a_mock.reset_mock()
assert_attrs(a_mock)

def test_assert_awaited(self):
with self.assertRaises(AssertionError):
Expand Down Expand Up @@ -578,20 +740,20 @@ def test_assert_awaited_once_with(self):

def test_assert_any_wait(self):
with self.assertRaises(AssertionError):
self.mock.assert_any_await('NormalFoo')
self.mock.assert_any_await('foo')

asyncio.run(self._runnable_test('foo'))
asyncio.run(self._runnable_test('baz'))
with self.assertRaises(AssertionError):
self.mock.assert_any_await('NormalFoo')
self.mock.assert_any_await('foo')

asyncio.run(self._runnable_test('NormalFoo'))
self.mock.assert_any_await('NormalFoo')
asyncio.run(self._runnable_test('foo'))
self.mock.assert_any_await('foo')

asyncio.run(self._runnable_test('SomethingElse'))
self.mock.assert_any_await('NormalFoo')
self.mock.assert_any_await('foo')

def test_assert_has_awaits_no_order(self):
calls = [call('NormalFoo'), call('baz')]
calls = [call('foo'), call('baz')]

with self.assertRaises(AssertionError) as cm:
self.mock.assert_has_awaits(calls)
Expand All @@ -601,7 +763,7 @@ def test_assert_has_awaits_no_order(self):
with self.assertRaises(AssertionError):
self.mock.assert_has_awaits(calls)

asyncio.run(self._runnable_test('NormalFoo'))
asyncio.run(self._runnable_test('foo'))
with self.assertRaises(AssertionError):
self.mock.assert_has_awaits(calls)

Expand Down Expand Up @@ -636,19 +798,19 @@ async def _custom_mock_runnable_test(*args):
mock_with_spec.assert_any_await(ANY, 1)

def test_assert_has_awaits_ordered(self):
calls = [call('NormalFoo'), call('baz')]
calls = [call('foo'), call('baz')]
with self.assertRaises(AssertionError):
self.mock.assert_has_awaits(calls, any_order=True)

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

asyncio.run(self._runnable_test('foo'))
asyncio.run(self._runnable_test('bamf'))
with self.assertRaises(AssertionError):
self.mock.assert_has_awaits(calls, any_order=True)

asyncio.run(self._runnable_test('NormalFoo'))
asyncio.run(self._runnable_test('foo'))
self.mock.assert_has_awaits(calls, any_order=True)

asyncio.run(self._runnable_test('qux'))
Expand Down
3 changes: 2 additions & 1 deletion Lib/unittest/test/testmock/testmock.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,7 @@ def test():
def test_setting_call(self):
mock = Mock()
def __call__(self, a):
self._increment_mock_call(a)
return self._mock_call(a)

type(mock).__call__ = __call__
Expand Down Expand Up @@ -2017,7 +2018,7 @@ def trace(frame, event, arg): # pragma: no cover
)

mocks = [
Mock, MagicMock, NonCallableMock, NonCallableMagicMock
Mock, MagicMock, NonCallableMock, NonCallableMagicMock, AsyncMock
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been thinking to add this. Thanks for doing it. It also needs to be added for EventMock in the other PR just to make sure there is no regression.

]

for mock in mocks:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Changes AsyncMock call count and await count to be two different counters.
Now await count only counts when a coroutine has been awaited, not when it
has been called, and vice-versa. Update the documentation around this.