Mocking aiohttp ClientSession contextmanager using asynctest - python

I have the following async function:
async def my_func(request):
# preparation code here
with aiohttp.ClientSession() as s:
with s.post(url, headers) as response:
status_code = response.status
if status_code == 200:
json_resp = await response.json()
elif:
# more checks
Trying to test it but still haven't found a way.
from asynctest.mock import CoroutineMock, MagicMock as AsyncMagicMock
#mock.patch("path_to_function.aiohttp.ClientSession", new_callable=AsyncMagicMock) # this is unittest.mock
def test_async_function(self, mocked_session):
s = AsyncMagicMock()
mocked_client_session().__aenter__ = CoroutineMock(side_effect=s)
session_post = s.post()
response_mock = AsyncMagicMock()
session_post.__aenter__ = CoroutineMock(side_effect=response_mock)
response_mock.status = 200
but not working as I want. Any help on how to test context managers would be highly appreciated.

Silly me, Found the solution. I was using side_effect instead of return_value.Works like a charm. Thank you very much

Related

Python Aiohttp: cookies behaviour on same domain

Hopefully this is not a too stupid question, but I am having trouble with aiohttp cookie processing.
Aiohttp's CookieJar class mentions it implements cookie storage adhering to RFC 6265, which states that:
cookies for a given host are shared across all the ports on that host
Cookies do not provide isolation by port. If a cookie is readable by a service running on one port, the cookie is also readable by a service running on another port of the same server.
But if I create two aiohttp servers, one that makes you "login" and gives you a cookie back, and another one with an endpoint that expects you to have a cookie, both hosted on localhost (two different ports I guess), the cookie will not be processed.
Here's a set of 4 tests using aiohttp, pytest, pytest and pytest-aiohttp to explain:
import functools
import pytest
from aiohttp import web
pytestmark = pytest.mark.asyncio
def attach_session(f):
#functools.wraps(f)
async def wrapper(request: web.Request):
session_id = request.cookies.get("testcookie")
request["mysession"] = session_id
response = await f(request)
response.set_cookie("testcookie", session_id)
return response
return wrapper
def is_logged_in(f):
#functools.wraps(f)
#attach_session
async def wrapper(request: web.Request):
session = request["mysession"]
if not session:
raise web.HTTPUnauthorized
return await f(request)
return wrapper
async def login(_: web.Request):
response = web.Response()
response.set_cookie("testcookie", "somerandomstring")
return response
#is_logged_in
async def some_endpoint(request: web.Request):
return web.Response(text="sweet")
#pytest.fixture
def auth_client(event_loop, aiohttp_client):
app = web.Application()
app.router.add_post("/login", login)
return event_loop.run_until_complete(aiohttp_client(app))
#pytest.fixture
def core_client(event_loop, aiohttp_client):
app = web.Application()
app.router.add_get("/some_endpoint", some_endpoint)
return event_loop.run_until_complete(aiohttp_client(app))
async def test_login(auth_client):
resp = await auth_client.post("/login")
assert resp.status == 200
assert resp.cookies.get("testcookie").value == "somerandomstring"
async def test_some_endpoint_anonymous(core_client):
resp = await core_client.get("/some_endpoint")
assert resp.status == 401
async def test_some_endpoint_as_logged_in(auth_client, core_client):
resp1 = await auth_client.post("/login")
resp2 = await core_client.get("/some_endpoint", cookies=resp1.cookies)
assert resp2.status == 401
async def test_some_endpoint_as_logged_in_again(auth_client, core_client):
resp1 = await auth_client.post("/login")
_cookie = list(resp1.cookies.values())[0]
resp2 = await core_client.get(
"/some_endpoint", cookies={_cookie.key: _cookie.value}
)
assert resp2.status == 200
But from my understanding, the "test_some_endpoint_as_logged_in" test should work. Why is it returning 401, while the same thing but with sending the cookie as a dict returns 200?
I think the correct way of sharing the cookies between clients would be loading the SimpleCookie object of the resp1 to the core_client.session.cookie_jar.
Changing the code of the test_some_endpoint_as_logged_in to should fix it:
async def test_some_endpoint_as_logged_in(auth_client, core_client):
resp1 = await auth_client.post("/login")
core_client.session.cookie_jar.update_cookies(resp1.cookies)
resp2 = await core_client.get("/some_endpoint")
assert resp2.status == 401
Cookie data is kept in the session object as the auth_client and core_client are different sessions with there own data cookie data is not shared. It is comparable to using a different browser with each there own cookie_jar.

Can we use Flask test_client() inside async function

I'm trying to test Flask REST-API end points with pytest using test_client(). But I'm getting an Error saying
> RuntimeError: You cannot use AsyncToSync in the same thread as an async event loop - just await the async function directly.
Can Anyone explain me why this happen and what is the solution to avoid this Error.
Test Functiob:
import pytest
from unittest import mock
from flask import request
from app import create_app
from app.base.views import views
app = create_app()
async def save_token_(sugar, payload, instance, bool, domain_id):
return {'valid':True}
payload = {
"password": 'password',
"username": 'crm_admin',
"grant_type": "password"
}
#pytest.mark.asyncio
async def test_post_sugar_token(monkeypatch, aiohttp_client, loop):
mock_save_token = mock.AsyncMock(name = "mock_save_token")
mock_save_token.return_value = await save_token_(None, payload, 'domain.org', True, 89)
monkeypatch.setattr(views, 'save_token', mock_save_token)
await views.save_token(None, payload, 'domain.org', True, 9)
assert mock_save_token.call_args_list == [mock.call(None, payload, 'domain.org', True, 89)]
headers = {'Autherization': 'ehrdmek2492.fkeompvmw.04294002'}
data = {
'password': '12345',
'key':'Hi',
'instance': 'my.domain',
'domain_id': 1
}
##-# using test_client()
client = app.test_client()
res = client.post('/token/sugar/', data = data, headers = headers)
assert res.status_code == 200
assert res.content_type == 'application/json'
assert res.json == {'valid':True}
# # ----------------------------------------------------------------------
Error Message
I ran into the same issue and opened a ticket on flask's github repo:
https://github.com/pallets/flask/issues/4375.
They kindly explained the issue and provided a workaround.
In short, flask can handle async views, and flask's test_client can be used in an async context, but you cannot use both at the same time.
Quoting from the github issue:
The problem in this case is that the Flask codebase is not compatible with asyncio, so you cannot run the test client inside an asyncio loop. There is really nothing to gain from writing your unit test as an async test, since Flask itself isn't async.
Here's the workaround suggested, slightly re-adjusted for your example:
#pytest.mark.asyncio
async def test_post_sugar_token():
# ... same code as before
##-# using test_client()
def sync_test():
with app.test_client() as client:
res = client.post('/token/sugar/', data = data, headers = headers)
assert res.status_code == 200
assert res.content_type == 'application/json'
assert res.json == {'valid':True}
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, sync_test)

Testing and mocking asynchronous code that uses async with statement

I have a simple asynchronous functions that fetches JSON from the web:
async def fetch (session : aiohttp.ClientSession, url : str, pageSize : int = 100, pageNumber : int = 0):
async with session.get(url, params={'pageSize' : f"{pageSize}", 'pageNumber' : f"{pageNumber}"}) as response:
return await response.json()
How do I write unit tests for it? I am learning how to do that in Python currently and I have settled down on using unittest and aiounittest modules. The scaffolding that I use for testing is as follows:
class AsyncTest(aiounittest.AsyncTestCase):
async def test_fetch(self):
mocked_session = ... #no idea how to create
json = await fetch(mocked_session, "url_for_mock", 10, 0)
I think I would have to somehow mock the session argument using unittest.Mock() functionality to return some harcoded json data but I am a little lost on how to do this so it works inside async with statement. I have never used an unit testing library in Python and an example would be very helpful here. I am using Python 3.8 and aiohttp for networking.
I was able to mock the asynchronous resource manager by implementing my own version of __aenter__ magic method and using AsyncMock to mock relevant async methods. The proof of concept test that mocks network calls looks like follows:
class AsyncTest(aiounittest.AsyncTestCase):
async def test_fetch(self):
request_mock = AsyncMock()
request_mock.__aenter__.return_value = request_mock
request_mock.json.return_value = { 'hello' : 'world'}
session = Mock()
session.get.return_value = request_mock
json = await fetch(session, "url_for_mock", 10, 0)

Asynchronous python requests.post()

So the idea is to collect responses for 1 million queries and store them in a dictionary. I want it to be asynchronous because requests.post takes 1 second for each query and I want to keep the loop going while it's wait for response. After some research I have something like this.
async def get_response(id):
query_json = id2json_dict[id]
response = requests.post('some_url', json = query_json, verify=false)
return eval(response.text)
async def main(id_list):
for unique_id in id_list:
id2response_dict[unique_id] = get_response(unique_id)
I know this is not asynchronous, how do I use "await" in it to make it truly asynchronous?
The requests-async pacakge provides asyncio support for requests... https://github.com/encode/requests-async
Either that or use aiohttp.
Not tested, but this should work:
async def get_response(id):
query_json = id2json_dict[id]
# We need to_thread here because requests.post is synchronous.
response = await asyncio.to_thread(
requests.post,
'some_url', json = query_json, verify=false
)
return eval(response.text)
async def main(id_list):
tasks = [
get_response(unique_id) for unique_id in id_list
]
results = await asyncio.gather(*tasks)
id2response_dict = {
unique_id: result
for (unique_id, result) in zip(id_list, results)
}

How to get dynamic path params from route in aiohttp when mocking the request?

Using the below route definition, I am trying to extract the book_id out of the URL in aiohttp.
from aiohttp import web
routes = web.RouteTableDef()
#routes.get('/books/{book_id}')
async def get_book_pages(request: web.Request) -> web.Response:
book_id = request.match_info.get('book_id', None)
return web.json_response({'book_id': book_id})
Below is the test (using pytest) I have written
import asynctest
import pytest
import json
async def test_get_book() -> None:
request = make_mocked_request('GET', '/books/1')
response = await get_book(request)
assert 200 == response.status
body = json.loads(response.body)
assert 1 == body['book_id']
Test Result:
None != 1
Expected :1
Actual :None
Outside of the tests, when I run a request to /books/1 the response is {'book_id': 1}
What is the correct way to retrieve dynamic values from the path in aiohttp when mocking the request?
make_mocked_request() knows nothing about an application and its routes.
To pass dynamic info you need to provide a custom match_info object:
async def test_get_book() -> None:
request = make_mocked_request('GET', '/books/1',
match_info={'book_id': '1'})
response = await get_book(request)
assert 200 == response.status
body = json.loads(response.body)
assert 1 == body['book_id']
P.S.
In general, I want to warn about mocks over-usage. Usually, functional testing with aiohttp_client is easier to read and maintain.
I prefer mocking for really hard-to-rest things like network errors emulation.
Otherwise your tests do test your own mocks, not a real code.

Categories