FastAPI: How to test APIs by overriding functions in `Depends()` - python

I am very, very new to FastAPI testing, so any guidance in the right direction would be appreciated.
So what I have right now is as follows:
A very simple routes file: datapoint_routes.py
from fastapi import APIRouter, Depends
datapoint_router = APIRouter()
def some_function_is():
return "Actual"
#datapoint_router.get('/{datapoint_name}')
def get_db(
datapoint_name: str,
some_function_output=Depends(some_function_is)
) -> dict:
return {
'datapoint_name': datapoint_name,
'state': some_function_output
}
I want to be able to test this. I checked out FastAPI Testing Dependencies guide here. But this did not help at all, because it didn't work for me.
For my tests, what I have right now is something like this:
File: test_datapoint_router.py
from typing import Union
from fastapi import FastAPI
from fastapi.testclient import TestClient
from datapoint_routes import datapoint_router, some_function_is
DATAPOINT_NAME = 'abcdef'
app = FastAPI()
client = TestClient(datapoint_router)
def override_dep(q: Union[str, None] = None):
return "Test"
app.dependency_overrides[some_function_is] = override_dep
def test_read_main():
response = client.get(f"/{DATAPOINT_NAME}")
assert response.status_code == 200
assert response.json() == {
'datapoint_name': DATAPOINT_NAME,
'state': "Test"
}
I would hope in the test, the response = client.get() would be based on the overriding function override_dep, which would replace some_function_is.
I thought the response.json() would be:
{
'datapoint_name': 'abcdef',
'state': 'Test'
}
instead, it is:
{
'datapoint_name': 'abcdef',
'state': 'Actual'
}
This means that the override_dep function in the test is useless.
I even checked out the value of app.dependency_overrides, and it shows a correct map:
(Pdb) app.dependency_overrides
{<function some_function_is at 0x102b3d1b0>: <function override_dep at 0x102b3e0e0>}
Where the memory values of functions do match:
(Pdb) some_function_is
<function some_function_is at 0x102b3d1b0>
(Pdb) override_dep
<function override_dep at 0x102b3e0e0>
What am I doing wrong?

You're creating the FastAPI app object in your test, but you're using a defined router with your TestClient. Since this router is never registered with the app, overriding a dependency with the app won't do anything useful.
The TestClient is usually used with the root app (so that the tests run against the app itself):
from fastapi import APIRouter, Depends, FastAPI
app = FastAPI()
datapoint_router = APIRouter()
def some_function_is():
return "Actual"
#datapoint_router.get('/{datapoint_name}')
def get_db(
datapoint_name: str,
some_function_output=Depends(some_function_is)
) -> dict:
return {
'datapoint_name': datapoint_name,
'state': some_function_output
}
app.include_router(datapoint_router)
And then the test:
from typing import Union
from fastapi.testclient import TestClient
from datapoint_routes import app, datapoint_router, some_function_is
DATAPOINT_NAME = 'abcdef'
client = TestClient(app)
def override_dep(q: Union[str, None] = None):
return "Test"
app.dependency_overrides[some_function_is] = override_dep
def test_read_main():
response = client.get(f"/{DATAPOINT_NAME}")
assert response.status_code == 200
assert response.json() == {
'datapoint_name': DATAPOINT_NAME,
'state': "Test"
}
This passes as expected, since you're now testing against the app (TestClient(app)) - the location where you overrode the dependency.

MatsLindh's answer does solve the problem, and I would like to suggest another improvement.
Overriding the depends function at the root of the test file, introduces a risk of interfering with the following tests, due to a lack of cleanup.
Instead, I suggest using a fixture, which would ensure the isolation of your tests. I wrote a simple pytest plugin to integrate with the dependency system of FastAPI to simplify the syntax as well.
Install it via: pip install pytest-fastapi-deps and then use it like so:
from typing import Union
from fastapi.testclient import TestClient
from datapoint_routes import app, datapoint_router, some_function_is
DATAPOINT_NAME = 'abcdef'
client = TestClient(app)
def override_dep(q: Union[str, None] = None):
return "Test"
def test_read_main_context_manager(fastapi_dep):
with fastapi_dep(app).override({some_function_is: override_dep}):
response = client.get(f"/{DATAPOINT_NAME}")
assert response.status_code == 200
assert response.json() == {
'datapoint_name': DATAPOINT_NAME,
'state': "Test"
}

Related

Testing celery.send_task() inside endpoint

I have this configuration (for demonstration purposes)
endpoints.py
celery_conf.py
Inside celery client is the configuration setup for celery, and inside endpoints.py there is for example an endpoint where celery_client is imported.
In endpoints.py I import celery_client (instantiated Celery() object)
#in endpoints.py
from celery_conf import celery_client
#router.post(
include_in_schema=True,
status_code=status.HTTP_200_OK,
name="some_name:post"
)
def foo_endpoint(
item: PydanticModel, db: Session = Depends(get_database)
) -> dict:
tmp = <some preprocessing of item>
celery_client.send_task(...)
return 200
I want to test this endpoint and see if celery_client.send_task() has been invoked.
How can i do this? I have read about pytest patch feature, but I do not understand how to test it.
Lets say I have this test:
client = TestClient() #fastapi test client
def test_enpoint():
#patch where celery client is imported
with patch('endpoints.celery_client') as mock_task:
client.put(url=app.url_path_for("some_name:post"), data={})
...
How do I test if celery_client.send_task() has been ivoked inside endpoint?
You can do this with:
with patch("endpoints.celery_client.send_task") as mock_task:
client.put(url=app.url_path_for("some_name:post"), data={})
assert mock_task.call_count == 1
assert mock_task.call_args
or there is also the pytest-mock package that can help:
def test_endpoint(mocker: MockerFixture):
mock_task = mocker.patch("endpoints.celery_client.send_task")
client.put(url=app.url_path_for("some_name:post"), data={})
mock_task.assert_called_once()
mock_task.assert_called_once_with(arg1, arg2)

How can I get every route path from FastAPI app?

I am new to FastAPI and Python. I need to get all the routes on my root path and show it to the user. However, I could not find a way to get all the paths recursively. The API is versioned with the help of VersionedFastAPI and the current code does not give the path inside version; it just returns generic ones.
FastAPI backend:
app = FastAPI()
router = APIRouter(
tags=["utilities"]
)
#router.get("/")
def read_root(request: Request):
url_list = [
route.path
for route in request.app.routes
]
return { "endpoints": set(url_list) }
#app.get('/foo')
#version(1)
def foo():
return "foo V1"
#app.get('/foo')
#version(2)
def foo():
return "foo V2"
app = VersionedFastAPI(app, enable_latest=True, version_format='{major}', prefix_format='/v{major}')
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
app.include_router(router)
Code for getting the path list found under \ route
url_list = [
route.path
for route in request.app.routes
]
return { "endpoints": set(url_list) }
This returns only:
["/v1/openapi.json","/v2/docs","/openapi.json","/v2/openapi.json","/v2","/","/redoc","/v1","/docs","/docs/oauth2-redirect","/v1/docs","/latest"]
However /foo end point is missing. Any clue on this will help.
Have a look at the example given below. Make sure to call get_routes() before passing the FastAPI instance to VersionedFastAPI. Also, in the example below, there is an endpoint (i.e., /greet) with no version specified. Thus, to make sure that such endpoints—if happen to be in the API— will be assigned a version, define a default value (might as well be the latest version) when attempting to get the version of the endpoint at this line: version = getattr(route.endpoint, "_api_version", (2, 0)). By accessing the OpenAPI documention at http://127.0.0.1:8000/v1/docs and http://127.0.0.1:8000/v2/docs, you will notice that /greet appears in both versions of the API; hence, it can be accessed by either using /v1/greet or /v2/greet. That is because it hadn't been given any specific version initially; however, using either of the two endpoints, requests will be dispatched to the same path operation function.
from fastapi import FastAPI, APIRouter
from fastapi_versioning import VersionedFastAPI, version
import uvicorn
app = FastAPI()
router = APIRouter()
all_routes =[]
def get_routes():
reserved_routes = ["/openapi.json", "/docs", "/docs/oauth2-redirect", "/redoc"]
for route in app.routes:
if route.path not in reserved_routes:
if route.name is not None:
version = getattr(route.endpoint, "_api_version", (2, 0))
all_routes.append("/v" + str(version[0]) + route.path)
#router.get("/")
def index():
return { "endpoints": all_routes }
#app.get("/foo")
#version(1)
def foo():
return "foo v1"
#app.get("/foo")
#version(2)
def foo():
return "foo v2"
#app.get("/items/{item_id}")
#version(2)
def get_item(item_id: int):
return item_id
#app.get("/greet")
def greet_with_hi():
return "Hi"
get_routes()
app = VersionedFastAPI(app, version_format='{major}',prefix_format='/v{major}')
app.include_router(router)
if __name__ == '__main__':
uvicorn.run(app, host='127.0.0.1', port=8000)
Output (when accessing http://127.0.0.1:8000/):
{"endpoints":["/v1/foo","/v2/foo","/v2/items/{item_id}","/v2/greet"]}

How to handle models when programmatically creating endpoints in FastAPI

The following examples both work just fine, the only issue is that mypy is complaing about create_operation.
Specifically I'm getting these errors:
Variable "model" is not valid as a type
model? has no attribute "dict"
Especially the second error doesn't make sense to me since pydantic.BaseModel definitely has a dict method. Is there a better way to annotate this?
from typing import Type
from pydantic import BaseModel
from fastapi import FastAPI, testclient
app = FastAPI()
client = testclient.TestClient(app)
class A(BaseModel):
foo: str
# regular way of creating an endpoint
#app.post("/foo")
def post(data: A):
assert data.dict() == {"foo": "1"}
# creating an endpoint programmatically
def create_operation(model: Type[BaseModel]):
#app.post("/bar")
def post(data: model):
assert data.dict() == {"foo": "1"}
create_operation(A)
assert client.post("/foo", json={"foo": 1}).status_code == 200
assert client.post("/bar", json={"foo": 1}).status_code == 200

how to do unit test for functions inside of Resource class in flask-restful?

I am quite new to unit testing and relatively new to RESTful API development as well. I am wondering how to do unit test for functions inside Resource class in flask restful? I can do unit test for the endpoint's response but I don't know how to do testing for the individual functions inside the endpoint's controller class.
Below is my application code. It has 3 files including test:
api.py
controller_foo.py
test_controller_foo.py
# api.py
from flask import Flask
from flask_restful import Api
from .controller_foo import ControllerFoo
def create_app(config=None):
app = Flask(__name__)
app.config['ENV'] ='development'
return app
application = app = create_app()
api = Api(app)
api.add_resource(ControllerFoo, '/ctrl')
if __name__ == "__main__":
app.run(debug=True)
# controller_foo.py
from flask_restful import Resource
from flask import request
class ControllerFoo(Resource):
"""
basically flask-restful's Resource method is a wrapper for flask's MethodView
"""
def post(self):
request_data = self.handle_request()
response = self.process_request(request_data)
return response
def handle_request(self):
json = request.get_json()
return json
def process_request(self, data):
# do some stuffs here
return {'foo': 'bar'}
I am using unittest
# test_controller_foo.py
import unittest
from api import app
from .controller_foo import ControllerFoo
# initiating class to try testing but I don't know how to start
ctrl = ControllerFoo()
class ControllerFooTestCase(unittest.TestCase):
def setUp(self):
self.app = app
self.app.config['TESTING'] = True
self.client = app.test_client()
self.payload = {'its': 'empty'}
def tearDown(self):
pass
def test_get_response(self):
response = self.client.post('/ctrl', json=self.payload)
expected_resp = {
'foo': 'bar'
}
self.assertEqual(response.status_code, 200)
self.assertDictEqual(response.get_json(), expected_resp)
if __name__ == "__main__":
unittest.main()
I want to know how to properly do unit test for handle_request and process_request function
EDIT: Fixing out my buggy code. Thanks Laurent LAPORTE for the highlights.
There are several bugs in your code, so this is not easy to explain.
First of all, the recommended way to do testing with Flask (and Flask-Restful) is to use PyTest instead of unittest, because it is easier to setup and use.
Take a look at the documentation: Testing Flask Applications.
But, you can start with unittest…
note: you can have a confusion with your app module and the app instance in that module. So, to avoid it, I imported the module. Another good practice is to name your test module against the tested module: "app.py" => "test_app.py". You can also have a confusion with the controller module and the controller instance. The best practice is to use a more precise name, like "controller_foo" or something else…
Here is a working unit test:
# test_app.py
import unittest
import app
class ControllerTestCase(unittest.TestCase):
def setUp(self):
self.app = app.app
self.app.config['TESTING'] = True
self.client = self.app.test_client()
self.payload = {'its': 'empty'}
def test_get_response(self):
response = self.client.post('/ctrl', json=self.payload)
expected_resp = {'foo': 'bar'}
self.assertEqual(response.status_code, 200)
self.assertDictEqual(response.get_json(), expected_resp)
if __name__ == "__main__":
unittest.main()
As you can see, I also fixed the posted URL, in your application, the URL is "/ctrl", not "controller".
At this point, the test can run, but you have another error:
Ran 1 test in 0.006s
FAILED (errors=1)
Error
Traceback (most recent call last):
...
TypeError: process_request() takes 1 positional argument but 2 were given
If you take a look at your process_request() method, you can see that you missed the self parameter. Change it like this.
def process_request(self, data):
# do some stuffs here
return {'foo': 'bar'}
Your test should pass.
But, that not the right way to implement Flask-Restful controolers. Read the doc and use get and post methods…

How to apply integration tests to a Flask RESTful API

[As per https://stackoverflow.com/a/46369945/1021819, the title should refer to integration tests rather than unit tests]
Suppose I'd like to test the following Flask API (from here):
import flask
import flask_restful
app = flask.Flask(__name__)
api = flask_restful.Api(app)
class HelloWorld(flask_restful.Resource):
def get(self):
return {'hello': 'world'}
api.add_resource(HelloWorld, '/')
if __name__ == "__main__":
app.run(debug=True)
Having saved this as flaskapi.py and run it, in the same directory I run the script test_flaskapi.py:
import unittest
import flaskapi
import requests
class TestFlaskApiUsingRequests(unittest.TestCase):
def test_hello_world(self):
response = requests.get('http://localhost:5000')
self.assertEqual(response.json(), {'hello': 'world'})
class TestFlaskApi(unittest.TestCase):
def setUp(self):
self.app = flaskapi.app.test_client()
def test_hello_world(self):
response = self.app.get('/')
if __name__ == "__main__":
unittest.main()
Both the tests pass, but for the second test (defined in the TestFlaskApi) class I haven't yet figured out how to assert that the JSON response is as expected (namely, {'hello': 'world'}). This is because it is an instance of flask.wrappers.Response (which is probably essentially a Werkzeug Response object (cf. http://werkzeug.pocoo.org/docs/0.11/wrappers/)), and I haven't been able to find an equivalent of the json() method for requests Response object.
How can I make assertions on the JSON content of the second response?
Flask provides a test_client you can use in your tests:
from source.api import app
from unittest import TestCase
class TestIntegrations(TestCase):
def setUp(self):
self.app = app.test_client()
def test_thing(self):
response = self.app.get('/')
assert <make your assertion here>
Flask Testing Docs
I've found that I can get the JSON data by applying json.loads() to the output of the get_data() method:
import unittest
import flaskapi
import requests
import json
import sys
class TestFlaskApiUsingRequests(unittest.TestCase):
def test_hello_world(self):
response = requests.get('http://localhost:5000')
self.assertEqual(response.json(), {'hello': 'world'})
class TestFlaskApi(unittest.TestCase):
def setUp(self):
self.app = flaskapi.app.test_client()
def test_hello_world(self):
response = self.app.get('/')
self.assertEqual(
json.loads(response.get_data().decode(sys.getdefaultencoding())),
{'hello': 'world'}
)
if __name__ == "__main__":
unittest.main()
Both tests pass as desired:
..
----------------------------------------------------------------------
Ran 2 tests in 0.019s
OK
[Finished in 0.3s]
What you're doing there is not unit testing. In every case, when using the requests library or the flask client, you're doing integration testing as you make actual http calls to the endpoints and test the interaction.
Either the title of the question or the approach is not accurate.
With Python3, I got the error TypeError: the JSON object must be str, not bytes. It is required to decode:
# in TestFlaskApi.test_hello_world
self.assertEqual(json.loads(response.get_data().decode()), {'hello': 'world'})
This question gives an explanation.
The response object from test_client has a get_json method.
There's no need for converting the response to json with json.loads.
class TestFlaskApi(unittest.TestCase):
def setUp(self):
self.app = flaskapi.app.test_client()
def test_hello_world(self):
response = self.app.get("/")
self.assertEqual(
response.get_json(),
{"hello": "world"},
)

Categories