Python - объект MagicMock не может быть использован в выражении 'await'
Когда я пытался замокировать асинхронную функцию в модуле unittest
с помощью MagicMock
, я получил следующую ошибку:
TypeError: object MagicMock can't be used in 'await' expression
Пример кода выглядит так:
# исходный код
class Service:
async def compute(self, x):
return x
class App:
def __init__(self):
self.service = Service()
async def handle(self, x):
return await self.service.compute(x)
# код теста
import asyncio
import unittest
from unittest.mock import patch
class TestApp(unittest.TestCase):
@patch('__main__.Service')
def test_handle(self, mock):
loop = asyncio.get_event_loop()
app = App()
res = loop.run_until_complete(app.handle('foo'))
app.service.compute.assert_called_with("foo")
if __name__ == '__main__':
unittest.main()
Как я могу исправить это с помощью встроенных библиотек Python 3?
5 ответ(ов)
Если вам нужен вариант с pytest-mock
, совместимый с < py3.8, я сделал что-то вроде этого.
class AsyncMock(MagicMock):
async def __call__(self, *args, **kwargs):
return super().__call__(*args, **kwargs)
def test_my_method(mocker):
my_mock = mocker.patch("path.to.mocked.thing", AsyncMock())
my_mock.return_value = [1, 2, 3]
assert my_method() == [1, 2, 3]
Определенно взял идею из решения Томаша!
Чтобы переопределить асинхронные классы, необходимо указать функции patch
, что return_value
должен быть AsyncMock
. Используйте следующий код:
@patch('__main__.Service', return_value=AsyncMock(Service))
def test_handle(self, mock):
loop = asyncio.get_event_loop()
app = App()
res = loop.run_until_complete(app.handle('foo'))
app.service.compute.assert_called_with("foo")
Таким образом, Service
будет экземпляром MagicMock
, но при вызове Service()
у вас будет возвращен экземпляр AsyncMock
класса Service
.
В Python 3.8 и выше вы можете использовать класс AsyncMock
, который позволяет создавать асинхронные мок-объекты. Вот пример, как это сделать:
async def test_that_mock_can_be_awaited():
mock = AsyncMock()
mock.x.return_value = 123
result = await mock.x()
assert result == 123
Класс AsyncMock
ведет себя так, что объект распознается как асинхронная функция, и результат вызова — это awaitable (объект, который можно ожидать).
Вот несколько примеров использования AsyncMock
:
>>> mock = AsyncMock()
>>> asyncio.iscoroutinefunction(mock)
True
>>> inspect.isawaitable(mock())
True
Как видно из приведенных выше примеров, AsyncMock
позволяет вам тестировать асинхронный код, имитируя поведение асинхронных функций.
Вы можете воспользоваться следующим обходным решением для создания асинхронного объекта MagicMock
, который будет корректно работать с await
. Вот как это можно сделать:
from unittest.mock import MagicMock
# Обходим (monkey patch) MagicMock
async def async_magic():
pass
MagicMock.__await__ = lambda self: async_magic().__await__()
Имейте в виду, что данное решение сработает только для MagicMock
и не будет работать с другими предопределенными значениями свойства return_value
. Если вам необходимо, чтобы и другие моки поддерживали асинхронный интерфейс, вам придется отдельно реализовать этот функционал для каждого из них.
shaun shia предложил действительно хорошее универсальное решение, но я обнаружил, что в Python 3.8 можно просто использовать @patch('__main__.Service', new=AsyncMock)
.
Как протестировать, что функция Python вызывает исключение?
Где размещать юнит-тесты на Python? [закрыто]
Работают ли параметризованные тесты pytest с тестами на основе классов unittest?
Способ вывести имя теста PyUnit в методе setup()
Мокирование класса против мокирования его интерфейса