Как макетировать aiohttp.client.ClientSession.get асинхронный контекстный менеджер
У меня есть некоторые проблемы с издевательством менеджера контекста aiohttp.client.ClientSession.get. Я нашел несколько статей, и вот один пример, который, кажется, работал: статья 1
Итак, мой код, который я хочу проверить:
async_app.py
import random
from aiohttp.client import ClientSession
async def get_random_photo_url():
while True:
async with ClientSession() as session:
async with session.get('random.photos') as resp:
json = await resp.json()
photos = json['photos']
if not photos:
continue
return random.choice(photos)['img_src']
И проверить:
test_async_app.py
from asynctest import CoroutineMock, MagicMock, patch
from asynctest import TestCase as TestCaseAsync
from async_app import get_random_photo_url
class AsyncContextManagerMock(MagicMock):
async def __aenter__(self):
return self.aenter
async def __aexit__(self, *args):
pass
class TestAsyncExample(TestCaseAsync):
@patch('aiohttp.client.ClientSession.get', new_callable=AsyncContextManagerMock)
async def test_call_api_again_if_photos_not_found(self, mock_get):
mock_get.return_value.aenter.json = CoroutineMock(side_effect=[{'photos': []},
{'photos': [{'img_src': 'a.jpg'}]}])
image_url = await get_random_photo_url()
assert mock_get.call_count == 2
assert mock_get.return_value.aenter.json.call_count == 2
assert image_url == 'a.jpg'
Когда я запускаю тест, я получаю сообщение об ошибке:
(test-0zFWLpVX) ➜ test python -m unittest test_async_app.py -v
test_call_api_again_if_photos_not_found (test_async_app.TestAsyncExample) ... ERROR
======================================================================
ERROR: test_call_api_again_if_photos_not_found (test_async_app.TestAsyncExample)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/kamyanskiy/.local/share/virtualenvs/test-0zFWLpVX/lib/python3.6/site-packages/asynctest/case.py", line 294, in run
self._run_test_method(testMethod)
File "/home/kamyanskiy/.local/share/virtualenvs/test-0zFWLpVX/lib/python3.6/site-packages/asynctest/case.py", line 351, in _run_test_method
self.loop.run_until_complete(result)
File "/home/kamyanskiy/.local/share/virtualenvs/test-0zFWLpVX/lib/python3.6/site-packages/asynctest/case.py", line 221, in wrapper
return method(*args, **kwargs)
File "/usr/lib/python3.6/asyncio/base_events.py", line 467, in run_until_complete
return future.result()
File "/home/kamyanskiy/.local/share/virtualenvs/test-0zFWLpVX/lib/python3.6/site-packages/asynctest/_awaitable.py", line 21, in wrapper
return await coroutine(*args, **kwargs)
File "/home/kamyanskiy/.local/share/virtualenvs/test-0zFWLpVX/lib/python3.6/site-packages/asynctest/mock.py", line 588, in __next__
return self.gen.send(None)
File "/home/kamyanskiy/work/test/test_async_app.py", line 23, in test_call_api_again_if_photos_not_found
image_url = await get_random_photo_url()
File "/home/kamyanskiy/work/test/async_app.py", line 9, in get_random_photo_url
json = await resp.json()
TypeError: object MagicMock can't be used in 'await' expression
----------------------------------------------------------------------
Ran 1 test in 0.003s
FAILED (errors=1)
Итак, я попытался отладить - вот что я вижу:
> /home/kamyanskiy/work/test/async_app.py(10)get_random_photo_url()
9 import ipdb; ipdb.set_trace()
---> 10 json = await resp.json()
11 photos = json['photos']
ipdb> resp.__aenter__()
<generator object CoroutineMock._mock_call.<locals>.<lambda> at 0x7effad980048>
ipdb> resp.aenter
<MagicMock name='get().__aenter__().aenter' id='139636643357584'>
ipdb> resp.__aenter__().json()
*** AttributeError: 'generator' object has no attribute 'json'
ipdb> resp.__aenter__()
<generator object CoroutineMock._mock_call.<locals>.<lambda> at 0x7effad912468>
ipdb> resp.json()
<MagicMock name='get().__aenter__().json()' id='139636593767928'>
ipdb> session
<aiohttp.client.ClientSession object at 0x7effb15548d0>
ipdb> next(resp.__aenter__())
TypeError: object MagicMock can't be used in 'await' expression
Итак, что является правильным способом для насмешки асинхронного менеджера контекста?
4 ответа
В вашей ссылке есть правка:
РЕДАКТИРОВАТЬ: проблема GitHub, упомянутая в этом посте, была решена, и с версии 0.11.1 asynctest поддерживает асинхронные менеджеры контекста из коробки.
поскольку asynctest==0.11.1
, это было изменено, рабочий пример:
import random
from aiohttp import ClientSession
from asynctest import CoroutineMock, patch
async def get_random_photo_url():
while True:
async with ClientSession() as session:
async with session.get('random.photos') as resp:
json = await resp.json()
photos = json['photos']
if not photos:
continue
return random.choice(photos)['img_src']
@patch('aiohttp.ClientSession.get')
async def test_call_api_again_if_photos_not_found(mock_get):
mock_get.return_value.__aenter__.return_value.json = CoroutineMock(side_effect=[
{'photos': []}, {'photos': [{'img_src': 'a.jpg'}]}
])
image_url = await get_random_photo_url()
assert mock_get.call_count == 2
assert mock_get.return_value.__aenter__.return_value.json.call_count == 2
assert image_url == 'a.jpg'
Критическая проблема заключается в том, что вам нужно правильно смоделировать функцию json
по умолчанию это MagicMock
пример. Чтобы получить доступ к этой функции, вам нужно mock_get.return_value.__aenter__.return_value.json
,
The
asynctest
не получал никаких обновлений с 2020 года, и каждый продолжает получать следующее уведомление об устаревании:
python3.9/site-packages/asynctest/mock.py:434
python3.9/site-packages/asynctest/mock.py:434: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
def wait(self, skip=0):
Вместо этого MagicMock можно использовать для имитации сопрограммы, как указано в документации :
Установка спецификации Mock или MagicMock для асинхронной функции приведет к тому, что объект сопрограммы будет возвращен после вызова.
Таким образом, вы можете легко использовать следующее:
from unittest.mock import MagicMock
@pytest.mark.asyncio
async def test_download():
mock = aiohttp.ClientSession
mock.get = MagicMock()
mock.get.return_value.__aenter__.return_value.status = 200
mock.get.return_value.__aenter__.return_value.text.return_value = 'test content'
async with aiohttp.ClientSession() as session:
async with session.get('http://test.com') as response:
assert response.text() == 'test content'
Вам не нужно устанавливать какой-либо фреймворк для тестирования
aiohttp.ClientSession
Проверьте это:
# Module A
import aiohttp
async def send_request():
async with aiohttp.ClientSession() as session:
async with session.post("https://example.com", json={"testing": True}) as response:
if response.status != 200:
print("Woops")
return False
print("YAY!")
return True
# Module A test
import pytest
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
# Do not forget to replace `path_to_module_a` with the module that imports aiohttp
import path_to_module_a.send_request
@patch("path_to_module_a.aiohttp.ClientSession")
@pytest.mark.asyncio
async def test_should_fail_to_send_request(mock: MagicMock):
session = MagicMock()
session.post.return_value.__aenter__.return_value = SimpleNamespace(status=500)
mock.return_value.__aenter__.return_value = session
response = await send_request()
assert response == False
assert session.post.call_args.kwargs["json"] == {"testing": True}
Основываясь на ответе @Sraw:
@pytest.mark.gen_test
@patch('application.adapters.http_retriever.aiohttp.ClientSession.get')
async def test_get_files(mock_get):
with open('tests/unit/sample_csv_files/Katapult_mock_response.json','r') as f:
json_dict = json.load(f)
mock_get.return_value.__aenter__.return_value.json = CoroutineMock()
mock_get.return_value.__aenter__.return_value.status = 200
mock_get.return_value.__aenter__.return_value.json.return_value = json_dict
Это сработало для меня