How to test #app.on_event("shutdown") in FastAPI? - python

I have a simple FastAPI setup as below,
# main.py
from fastapi import FastAPI
app = FastAPI()
#app.on_event("shutdown")
def app_shutdown():
with open("shutdown-test-file.txt", "w") as fp:
fp.write("FastAPI app has been terminated")
#app.get("/")
def root():
return {"message": "Hello World"}
How can I write (unit)test for this app_shutdown(...) functionality?
Related Posts
This SO post is also asking similar question, but, not in a "testing context"
The official doc has something similar, but, there is no example for on_event("shutdown")

According to the documentation, you need to wrap it in the context manager (a with statement) to trigger the events, something like this:
def test_read_items():
with TestClient(app) as client:
response = client.get("/items/foo")
assert response.status_code == 200
If you use pytest, you can set up a fixture for it like this:
from main import app
from fastapi.testclient import TestClient
import pytest
#pytest.fixture
def client():
with TestClient(app) as c:
yield c
def test_read_main(client):
response = client.get("/")
assert response.status_code == 200

Related

Creating Endpoints dynamically in Python

I am Test Developer and I'm trying to create basic HTTP Server mock app which could generate endpoints with using one "master endpoint" eg. /generate_endpoint.
I would provide url and body (and maybe response code later) to /generate_endpoint, and when I call endpoint which I created it will give me the "body" response.
It must work without restarting server, since I would like to use it multiple times with different urls and body's.
Below is code I tried for that.
If that isn't possible to dynamically create endpoints, then maybe you could give me advice - because I want to create Mock to test MyApp and the basic workflow is like that:
Check if order exists (MyApp)
MyApp connects to externalApi and checks if order exists (That i want to mock)
MyApp returns value based on what is given in externalApi
but there is multiple responses (and multiple endpoints) which might occur and I want to have test cases for them all so I will not need external app for my tests.
here is what I tried:
from fastapi import HTTPException
router = APIRouter()
endpoints = {}
def generate_route(url: str, body: dict):
async def route():
return body
router.get(path=url)(route)
endpoints[url] = body
#router.post("/generate_endpoint")
async def generate_endpoint(endpoint_data: dict):
endpoint_url = endpoint_data.get("url")
endpoint_body = endpoint_data.get("body")
if not endpoint_url or not endpoint_body:
raise HTTPException(status_code=400, detail="url and body required")
generate_route(endpoint_url, endpoint_body)
return {"message": f"route added for url {endpoint_url}"}
or
from flask_restful import Api, Resource, reqparse
app = Flask(__name__)
api = Api(app)
class GenerateEndpoint(Resource):
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("url", type=str)
parser.add_argument("response", type=str)
args = parser.parse_args()
def response():
return args["response"]
api.add_resource(response, args["url"])
return {"status": "success", "url": args["url"]}, 201
api.add_resource(GenerateEndpoint, "/generate_endpoints")
if __name__ == "__main__":
app.run(debug=True)
then im testing generate_endpoints with something like {"url": "/someurl", "body": "something"}
and then i Expect when i call GET 127.0.0.1:5000/someurl i will have "something" response
You can use this variant:
import uvicorn
from fastapi import FastAPI, APIRouter, HTTPException
router = APIRouter()
endpoints = {}
#router.get("/{url:path}")
def get_response(url: str):
if url in endpoints:
return endpoints[url]
raise HTTPException(status_code=404, detail="Not Found")
def generate_route(url: str, body: dict):
endpoints[url] = body
#router.post("/generate_endpoint")
async def generate_endpoint(endpoint_data: dict):
endpoint_url = endpoint_data.get("url")
endpoint_body = endpoint_data.get("body")
if not endpoint_url or not endpoint_body:
raise HTTPException(status_code=400, detail="url and body required")
generate_route(endpoint_url, endpoint_body)
return {"message": f"route added for url {endpoint_url}"}
app = FastAPI()
app.include_router(router)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=5000)

How to mock client IP on fastapi.testclient.TestClient?

Say you are using fastapi.testclient.TestClient to perform a GET, for instance. Inside the API code that defines that GET method, if you get request.client.host, you will get the string "testclient".
Test, using pytest:
def test_success(self):
client = TestClient(app)
client.get('/my_ip')
Now, lets assume your API code is something like this:
#router.get('/my_ip')
def my_ip(request: Request):
return request.client.host
The endpoit /my_ip is suppose to return the client IP, but when running pytest, it will return "testclient" string. Is there a way to change the client IP (host) on TestClient to something other than "testclient"?
You can mock the fastapi.Request.client property as,
# main.py
from fastapi import FastAPI, Request
app = FastAPI()
#app.get("/")
def root(request: Request):
return {"host": request.client.host}
# test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_main(mocker):
mock_client = mocker.patch("fastapi.Request.client")
mock_client.host = "192.168.123.132"
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"host": "192.168.123.132"}

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)

FastAPI - adding route prefix to TestClient

I have a FastAPI app with a route prefix as /api/v1.
When I run the test it throws 404. I see this is because the TestClient is not able to find the route at /ping, and works perfectly when the route in the test case is changed to /api/v1/ping.
Is there a way in which I can avoid changing all the routes in all the test functions as per the prefix? This seems to be cumbersome as there are many test cases, and also because I dont want to have a hard-coded dependency of the route prefix in my test cases. Is there a way in which I can configure the prefix in the TestClient just as we did in app, and simply mention the route just as mentioned in the routes.py?
routes.py
from fastapi import APIRouter
router = APIRouter()
#router.get("/ping")
async def ping_check():
return {"msg": "pong"}
main.py
from fastapi import FastAPI
from routes import router
app = FastAPI()
app.include_router(prefix="/api/v1")
In the test file I have:
test.py
from main import app
from fastapi.testclient import TestClient
client = TestClient(app)
def test_ping():
response = client.get("/ping")
assert response.status_code == 200
assert response.json() == {"msg": "pong"}
Figured out a workaround for this.
The TestClient has an option to accept a base_url, which is then urljoined with the route path. So I appended the route prefix to this base_url.
source:
url = urljoin(self.base_url, url)
However, there is a catch to this - urljoin concatenates as expected only when the base_url ends with a / and the path does not start with a /. This SO answer explains it well.
This resulted in the below change:
test.py
from main import app, ROUTE_PREFIX
from fastapi.testclient import TestClient
client = TestClient(app)
client.base_url += ROUTE_PREFIX # adding prefix
client.base_url = client.base_url.rstrip("/") + "/" # making sure we have 1 and only 1 `/`
def test_ping():
response = client.get("ping") # notice the path no more begins with a `/`
assert response.status_code == 200
assert response.json() == {"msg": "pong"}
The above work-around (by Shod) worked for me, but I had to pass the APIRouter object instead of FastAPI object to the testclient. I was receiving a 404 error otherwise.
Below is a sample code for how it worked for me.
from fastapi import FastAPI, APIRouter
from fastapi.testclient import TestClient
app = FastAPI()
router = APIRouter(prefix="/sample")
app.include_router(router)
#router.post("/s1")
def read_main():
return {"msg": "Hello World"}
client = TestClient(router)
client.base_url += "/sample"
client.base_url = client.base_url.rstrip("/") + "/"
def test_main():
response = client.post("s1")
assert response.status_code == 200
assert response.json() == {"msg": "Hello World"}

Unable to override dependency in FastAPI/FastAPi-Utils

This may be a newbie question. I am not able to override the greetings message in this simple 2 files FastAPI project. Could you please tell me what I might have done wrong? Thanks a lot for your help.
greetings_service.py
from fastapi import Depends
from fastapi_utils.cbv import cbv
from fastapi_utils.inferring_router import InferringRouter
router = InferringRouter()
def get_msg():
return "Original Message"
#cbv(router)
class GreetingsService:
#router.get("/")
async def greet(self, msg: str = Depends(get_msg)):
return f"Hello from FastAPI {msg}"
main.py
from fastapi import FastAPI
from starlette.testclient import TestClient
import greetings_service
app = FastAPI()
app.include_router(greetings_service.router)
def get_new_msg():
return "New Message"
//Tried this, doesn't work
#app.dependency_overrides["get_msg"] = get_new_msg()
//These 2 lines doesn't work too
app.dependency_overrides["get_msg"] = get_new_msg()
greetings_service.router.dependency_overrides_provider = app
client = TestClient(app)
res = client.get("/")
print(res.content) #"Hello from FastAPI Original Message" :(
The issue is with this:
app.dependency_overrides["get_msg"] = get_new_msg()
You are passing the dependency as string instead of the actual dependency.
Something like this would work:
from fastapi import FastAPI
from starlette.testclient import TestClient
import greetings_service
app = FastAPI()
app.include_router(greetings_service.router)
def get_new_msg():
return "New Message"
app.dependency_overrides[greetings_service.get_msg] = get_new_msg
client = TestClient(app)
res = client.get("/")
print(res.content)

Categories