0

Python - объект MagicMock не может быть использован в выражении 'await'

14

Когда я пытался замокировать асинхронную функцию в модуле 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 ответ(ов)

0

Если вам нужен вариант с 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]

Определенно взял идею из решения Томаша!

0

Чтобы переопределить асинхронные классы, необходимо указать функции 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.

0

В 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 позволяет вам тестировать асинхронный код, имитируя поведение асинхронных функций.

0

Вы можете воспользоваться следующим обходным решением для создания асинхронного объекта 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. Если вам необходимо, чтобы и другие моки поддерживали асинхронный интерфейс, вам придется отдельно реализовать этот функционал для каждого из них.

0

shaun shia предложил действительно хорошее универсальное решение, но я обнаружил, что в Python 3.8 можно просто использовать @patch('__main__.Service', new=AsyncMock).

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь