How to mock a method within an async unit test? - python

I have a class called database.py with a function called generate_token().
I would like to mock it and return a fixed value 321. So that I can see that the method was called and the return value returned.
How do I mock that? This is what I have tried.
#pytest.mark.asyncio
async def test_successful_register_returns_device_token(monkeypatch):
async def mock_generate_token():
return "321"
m = AsyncMock(mock_generate_token)
m.return_value = "321"
async with AsyncClient(app=app, base_url="http://127.0.0.1") as ac:
monkeypatch.setattr(database, "generate_token", m)
response = await ac.post(
"/register/",
headers={},
json={},
)
assert response.status_code == 201
assert "device_token" in response.json()
assert response.json()["device_token"] == "321"

It's actually much simpler than I thought, a normal #patch from from unittest.mock import patch is sufficient. It recognises the async methods and injects an AsyncMock automatically.
#pytest.mark.asyncio
#patch("service.auth_service.AuthService.generate_token")
async def test_successful_register_returns_device_token(self, mock_token):
mock_token.return_value = "321"
async with AsyncClient(app=app, base_url="http://testserver") as ac:
response = await ac.post(
"/register/",
headers={},
json={},
)
assert response.status_code == 201
assert "device_token" in response.json()
assert response.json()["device_token"] == "321"

Related

how to assert that fastAPI cache is working?

So I'm writing a function that use fastAPI cache to avoid making a bunch of post calls, then I'm wondering if is possible to write a test to validate that the functions is just called once and then reuse the cached value.
from fastapi_cache.decorator import cache
#cache(expire=60)
async def get_auth_token() -> str:
## just to exemplify
return "token"
class TestAuth(IsolatedAsyncioTestCase):
async def test_get_token_success(self):
"""Test get token"""
# success
_token = await get_auth_token()
assert _token is not None
assert _token == "token"
# here is the trick
# calling again should not call post again
_token = await get_auth_token()
mock_post.assert_called_once()
but I'm getting this error
AssertionError: Expected 'post' to have been called once. Called 2 times.
Another idea: return token with timestamp, that you can check timestamp to verify whether it is get from function or cache.
For example:
import time
from fastapi_cache.decorator import cache
#cache(expire=60)
async def get_auth_token() -> str:
## just to exemplify
return str(time.time())
class TestAuth(IsolatedAsyncioTestCase):
async def test_get_token_success(self):
"""Test get token"""
timestamp = str(time.time())
# success
token1 = await get_auth_token()
assert token1 is not None
assert timestamp <= token1 <= str(time.time())
# here is the trick
# calling again should not call post again
token2 = await get_auth_token()
assert token1 == token2
# check expire
time.sleep(60)
token3 = await get_auth_token()
assert token3 != token1

How to mock async response

I'm trying to test an async request but I didn't find how to do. I tried with patch decorator, with AsyncMock... Everytime, I had either aexit error or AsyncMock can't be used in await expression... Where am I wrong ?
class RequestService:
async def requestPostPicture(self, session: aiohttp.ClientSession, photoData: dict):
try:
with aiohttp.MultipartWriter('form-data') as mpwriter:
part = mpwriter.append(photoData['file'][1],{'content-type': photoData['file'][2]})
part.set_content_disposition('form-data', name='file', filename=photoData['file'][0])
async with session.post('https://www.api-url.com', data=mpwriter, headers=self.headers) as resp:
if isinstance(resp, dict):
return resp
apiResponse = await resp.json
return apiResponse
except Exception as error:
return {'error': str(error)}
My test :
class TestRequestService(IsolatedAsyncioTestCase):
#patch('aiohttp.ClientSession.post')
async def testRequestPostPictureDict(self, mockPost):
mockPost.__aenter__.return_value = {"error": "test"}
requestservice = RequestService()
pictureTest = {'file': ('photodatatest.jpg', 'photodatatest', 'image/jpeg')}
connector = aiohttp.TCPConnector(limit=15)
async with aiohttp.ClientSession(connector=connector) as sessionPicture:
returnValue = await requestservice.requestPostPicture(sessionPicture, pictureTest)
self.assertEqual(returnValue, {'error': 'test'})
async def testRequestPostPictureDict(self):
mock = aiohttp.ClientSession
mock.post = MagicMock()
mock.post.return_value.__aenter__.return_value = {'error': 'test'}

mock object creation in test

I have a function that looks like this
async def get_cluster_fee(self, request: web.Request):
cluster_type = request.rel_url.query[constants.CLUSTER_TYPE]
response = ClusterFeeModel().get_cluster_fee(cluster_type)
return web.json_response(response)
To test it I have implemented the test
async def test_get_cluster_fee_success(self, loop, app_client):
client, app = app_client
path = "/bm-shop-bff/v2/cluster-fee?type=CLS1"
with patch("src.api.v2.cpq.clusterfee.model.ClusterFeeModel", new=ClusterFeeModelStub):
response = await client.session.get(url=f"http://{client.host}:{client.port}{path}")
assert 200 == response.status
response_data = await response.json()
assert response_data["cluster_fee"] and response_data["cluster_type"]
I want to mock ClusterFeeModel to return a specific get_cluster_fee, but I don't want to instantiate ClusterFeeModel object during execution (as it tries to connect to external systems in constructor).
I have created a stub class for that
class ClusterFeeModelStub:
def get_cluster_fee(self, cluster_type):
return {"cluster_type": "CLS1", "cluster_fee": 1.1}
However with my patch it still tries to instantiate ClusterFeeModel object. What is the problem?

How to mock simultaneous requests that are been made with asyncio.gather using pytest, aiohttp and aioresponses?

I have a piece of code which uses asyncio.gather to make simultaneous requests:
estimated_income, judicial_records = await asyncio.gather(
*(client.get_estimated_income(), client.get_judicial_records()), return_exceptions=True
)
# `client.get_estimated_income()` calls `CREDIT_BUREAU_URL`
# `client.get_judicial_records()` calls `NATIONAL_ARCHIVES_URL`
In my tests I'm trying to simulate some scenarios by mocking the requests status:
mock_aioresponse.get(NATIONAL_ARCHIVES_URL, status=200)
mock_aioresponse.get(CREDIT_BUREAU_URL, status=400)
If I run a single test, it works as expected but if I run more than one (and the others don't even have to use mock_aioresponse) I reach that piece of code twice and start to get some Connection refused errors in the second time (the first one works just fine) - which propagates to the tests making they fail.
The weirdest thing to me is reaching that function twice if I run more than one test.
How can I use aioresponses to accomplish my test cases?
CODE:
# main.py
#app.get(
"/leads/{lead_id}/sales_pipeline",
response_model=LeadRead,
responses={status.HTTP_404_NOT_FOUND: {"model": NotFoundResponse}},
)
def sales_pipeline(lead_id: int, db: Session = Depends(get_db)):
lead = db.get(Lead, lead_id)
if not lead:
raise HTTPException(status_code=404, detail="Lead not found")
pipeline_history = PipelineHistory(lead_id=lead.id)
db.add(pipeline_history)
db.commit()
db.refresh(pipeline_history)
# dispatch an event to handlers.py
dispatch(event_name=SALES_PIPELINE_ENTRYPOINT_EVENT_NAME, payload={"id": pipeline_history.id})
return lead
# handlers.py
async def _check_if_lead_is_able_to_become_prospect(
client: LeadExternalSystemsClient,
) -> Tuple[Optional[bool], Optional[str]]:
error_messages: List[str] = []
estimated_income, judicial_records = await asyncio.gather(
*(client.get_estimated_income(), client.get_judicial_records()), return_exceptions=True
)
if isinstance(estimated_income, LeadExternalSystemsClient.LeadExternalSystemsException):
error_messages.append("Credit Bureau network error")
if isinstance(judicial_records, LeadExternalSystemsClient.LeadExternalSystemsException):
error_messages.append("National Archives network error")
# more code
# `LeadExternalSystemsClient` class at client.py
class LeadExternalSystemsClient:
class LeadExternalSystemsException(Exception):
pass
def __init__(self, lead: Lead, timeout: int = 30):
self.lead = lead
self._session = ClientSession(
timeout=ClientTimeout(total=timeout),
connector=TCPConnector(limit=30, ssl=False),
raise_for_status=True,
)
async def __aenter__(self) -> "LeadExternalSystemsClient":
return self
async def __aexit__(self, *_, **__) -> None:
await self._session.close()
async def _request(self, method: str, url: str) -> Any:
try:
response = self._session.request(method=method, url=url)
return await response.json()
except ClientError as exception:
raise self.LeadExternalSystemsException(str(exception))
async def get_estimated_income(self) -> Dict[str, float]:
result = await self._request(method="GET", url=CREDIT_BUREAU_URL)
# more code
async def get_judicial_records(self) -> List[Dict[str, str]]:
result = await self._request(method="GET", url=NATIONAL_ARCHIVES_URL)
# more code
# tests
#pytest.mark.usefixtures("mock_engine_for_test")
def test_estimated_income_network_error(client, lead, mocker, mock_aioresponse):
# GIVEN
mocker.patch(
"app.consumers.handlers.LeadExternalSystemsClient.personal_information_is_valid",
return_value=True,
)
mock_aioresponse.get(NATIONAL_ARCHIVES_URL, status=200)
mock_aioresponse.get(CREDIT_BUREAU_URL, status=400)
# WHEN
response = client.get(f"/leads/{lead.id}/sales_pipeline")
result = client.get(f"/leads/{lead.id}").json()
# THEN
assert response.status_code == status.HTTP_200_OK
assert result["is_prospect"] is False
assert len(result["pipeline_histories"]) == 1
assert result["pipeline_histories"][0]["started_at"] is not None
assert result["pipeline_histories"][0]["finished_at"] is not None
assert result["pipeline_histories"][0]["extra_infos"] == "Credit Bureau network error"
assert result["pipeline_histories"][0]["status"] == PipelineStatus.NETWORK_ERROR.name
Looks like the solution is to pass repeat=True to aioresponses().get()
https://bytemeta.vip/repo/pnuckowski/aioresponses/issues/205

How can solve this deprecationWarning?

I want multi requests using aiohttp.
I was wrapping aiohttp like this, and i was test like this
my code
import asyncio
from aiohttp import ClientSession as AioClientSession
class ClientSession(AioClientSession):
async def _get(self, session, url, params=None, **kwargs):
async with session.get(url, params=params, **kwargs) as response:
return await response.json()
async def _post(self, session, url, data=None, **kwargs):
async with session.post(url, data=data, **kwargs) as response:
return await response.json()
async def fetch_all(self, method, urls, loop, data=None, params=None, **kwargs):
async with AioClientSession(loop=loop) as session:
if method == "GET":
results = await asyncio.gather(*[self._get(session, url, params=params, **kwargs) for url in urls])
elif method == "POST":
results = await asyncio.gather(*[self._post(session, url, data=data, **kwargs) for url in urls])
else:
assert False
return results
def multi_requests_get(urls, params=None, **kwargs):
session = ClientSession()
loop = asyncio.get_event_loop()
result = loop.run_until_complete(session.fetch_all("GET", urls, loop, params=params, **kwargs))
session.close()
return result
def multi_requests_post(urls, data=None, **kwargs):
session = ClientSession()
loop = asyncio.get_event_loop()
result = loop.run_until_complete(session.fetch_all("POST", urls, loop, data=data, **kwargs))
session.close()
return result
test code
urls = ["https://httpbin.org/get?{}={}".format(x, x) for x in range(10)]
result = multi_requests_get(urls=urls)
assert result
assert result[0]["args"] == {"0": "0"}
assert result[1]["args"] == {"1": "1"}
but this test return Warning like this:
The object should be created from async function
loop=loop)
How can I avoid this warning?
Here is the full traceback
============================================================================= warnings summary ==============================================================================
base/tests/test_aiohttp.py::AioHttpTest::test_get
/path/server/base/requests.py:122: DeprecationWarning: The object should be created from async function
session = ClientSession()
base/tests/test_aiohttp.py::AioHttpTest::test_get
base/tests/test_aiohttp.py::AioHttpTest::test_post
/env_path/lib/python3.6/site-packages/aiohttp/connector.py:730: DeprecationWarning: The object should be created from async function
loop=loop)
base/tests/test_aiohttp.py::AioHttpTest::test_get
base/tests/test_aiohttp.py::AioHttpTest::test_post
/env_path/lib/python3.6/site-packages/aiohttp/connector.py:735: DeprecationWarning: The object should be created from async function
resolver = DefaultResolver(loop=self._loop)
base/tests/test_aiohttp.py::AioHttpTest::test_get
base/tests/test_aiohttp.py::AioHttpTest::test_post
/env_path/lib/python3.6/site-packages/aiohttp/cookiejar.py:55: DeprecationWarning: The object should be created from async function
super().__init__(loop=loop)
base/tests/test_aiohttp.py::AioHttpTest::test_get
/path/darae/server/base/requests.py:125: RuntimeWarning: coroutine 'ClientSession.close' was never awaited
session.close()
base/tests/test_aiohttp.py::AioHttpTest::test_post
/path/server/base/requests.py:131: DeprecationWarning: The object should be created from async function
session = ClientSession()
base/tests/test_aiohttp.py::AioHttpTest::test_post
/path/server/base/requests.py:134: RuntimeWarning: coroutine 'ClientSession.close' was never awaited
session.close()
-- Docs: https://docs.pytest.org/en/latest/warnings.html
=================================================================== 2 passed, 10 warnings in 1.93 seconds ===================================================================
You may take a look at the simple working example from here and find a place where to place your aiohttp.ClientSession() as client:
import aiohttp
import asyncio
async def fetch(client):
async with client.get('http://python.org') as resp:
assert resp.status == 200
return await resp.text()
async def main():
async with aiohttp.ClientSession() as client:
html = await fetch(client)
print(html)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
aiohttp.ClientSession class MUST be instantiated inside coroutine function, not just a function.
All you need to do:
Remove parent class from your ClientSession - you already use it explicitly in your fetch_all and you don't need it there anymore.
Remove calls of session.close() - session DO close automatically by context manager in fetch_all.

Categories

Resources