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()
Мокирование класса против мокирования его интерфейса