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"}
Related
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)
I'm writing test for a FastAPI application. When I write test for an endpoint with GET method everything works as expected, but when I call an endpoint with POST method somehow my request gets redirected to http://testserver this is an example of my endpoints:
from json import JSONDecodeError
from fastapi import APIRouter
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.status import HTTP_400_BAD_REQUEST
router = APIRouter()
#router.post("/test")
async def test(
request: Request,
):
try:
body = await request.json()
except JSONDecodeError:
return JSONResponse(content={}, status_code=HTTP_400_BAD_REQUEST)
return JSONResponse(content=body)
and this is an example of my test:
from starlette.testclient import TestClient
from app import app
client = TestClient(app)
def test_cookies():
res = client.post(
"api/test/",
json={
"name": "test"
},
)
assert 200 == res.status_code
again this happens just with POST and PUT requests the GET request works just fine. any idea why is this happening?
Your endpoint is registered as /api/test, while you're calling /api/test/ - notice the difference in the trailing slash.
By default FastAPI will issue a redirect to make your browser talk to the correct endpoint. The http://testserver URL you're seeing is the internal hostname used in the TestClient.
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
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"}
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)