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
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
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'}
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?
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
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.