Unittesting FastAPI CRUD doesn't work as expected - unit-testing

I have a problem with unit tests in a project that use FastAPI.
I used TestClient, pytest, monkeypatch.
When I test a GET function I don't know why but instead to have the result of the mocked function the result come from the "real" service, getting the results from the DB.
The function to test:
from fastapi import APIRouter, Body
...
...
router = APIRouter()
#router.post('/', response_description="feature data added into the database")
async def add_feature_data(feature: FeatureSchema = Body(...)):
feature = jsonable_encoder(feature)
new_feature = await add_feature(feature)
return ResponseModel(new_feature, "feature added successfully.")
my file conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import klingon
#pytest.fixture(scope='module')
def test_app():
client = TestClient(klingon)
yield client
the test file:
import json
import pytest
from app.server.routes import feature_route
def test_get_features(test_app, monkeypatch):
test_data = [
{
'change': '1',
'urs': 'something',
'state': 'active',
'created_at': '2020-11-09T11:51:14Z',
'created_by': '9'
},
{
'change': '2',
'urs': 'something 2',
'state': 'active',
'created_at': '2020-10-16T11:51:14Z',
'created_by': '3'
}
]
async def mock_get_features():
return test_data
monkeypatch.setattr(feature_route, 'get_features', mock_get_features)
response = test_app.get('/feature/')
assert response.status_code == 200
assert response.json() == test_data
Thanks

Related

Best way to override FastAPI dependencies for testing with a different dependency for each test

According to FastAPI official documentation the recommended way to override the dependencies for testing is to do it globally before all tests are run:
async def override_dependency(q: Optional[str] = None):
return {"q": q, "skip": 5, "limit": 10}
app.dependency_overrides[common_parameters] = override_dependency
def test_override_in_items():
response = client.get("/items/")
assert response.status_code == 200
assert response.json() == {
"message": "Hello Items!",
"params": {"q": None, "skip": 5, "limit": 10},
}
def test_override_in_items_with_q():
response = client.get("/items/?q=foo")
assert response.status_code == 200
assert response.json() == {
"message": "Hello Items!",
"params": {"q": "foo", "skip": 5, "limit": 10},
}
But this lets you override the dependency just once for the entire test run. What if I need to have different dependencies for each test? Is it safe to override the dependency inside of the test body and reset them after the test has finished? Like this
def test_override_in_items():
app.dependency_overrides[common_parameters] = override_dependency
response = client.get("/items/")
assert response.status_code == 200
assert response.json() == {
"message": "Hello Items!",
"params": {"q": None, "skip": 5, "limit": 10},
}
app.dependency_overrides[common_parameters] = {}
What are the drawbacks of doing it like the example above?
This is what I use myself to override dependencies in my tests. You have to use it as a context manager with a with statement (See examples below). Upon entering the context it saves existing overrides it encounters. These will be restored when exiting the context.
import typing
from fastapi import FastAPI
class DependencyOverrider:
def __init__(
self, app: FastAPI, overrides: typing.Mapping[typing.Callable, typing.Callable]
) -> None:
self.overrides = overrides
self._app = app
self._old_overrides = {}
def __enter__(self):
for dep, new_dep in self.overrides.items():
if dep in self._app.dependency_overrides:
# Save existing overrides
self._old_overrides[dep] = self._app.dependency_overrides[dep]
self._app.dependency_overrides[dep] = new_dep
return self
def __exit__(self, *args: typing.Any) -> None:
for dep in self.overrides.keys():
if dep in self._old_overrides:
# Restore previous overrides
self._app.dependency_overrides[dep] = self._old_overrides.pop(dep)
else:
# Just delete the entry
del self._app.dependency_overrides[dep]
You can use it like this in your tests:
from app.main import app
from fastapi.testclient import TestClient
def test_override_in_items(client: TestClient):
with DependencyOverrider(app, overrides={common_parameters: override_dependency}):
response = client.get("/items/")
assert response.status_code == 200
assert response.json() == {
"message": "Hello Items!",
"params": {"q": None, "skip": 5, "limit": 10},
}
Or one could use it as a fixture in pytest:
#pytest.fixture(scope="function")
def faked_common_parameters():
with DependencyOverrider(
app, overrides={common_parameters: override_dependency}
) as overrider:
yield overrider
def test_override_in_items(client: TestClient, faked_common_parameters):
# Running with the overridden dependency
response = client.get("/items/")
assert response.status_code == 200
assert response.json() == {
"message": "Hello Items!",
"params": {"q": None, "skip": 5, "limit": 10},
}
I used Mat's answer and created an open-source library that adds a fixture based on the code snippet. See it here.
To use the library simply do: pip install pytest-fastapi-deps, then you'll have the fastapi_dep fixture. Use it like so:
import pytest
from fastapi.testclient import TestClient
client = TestClient(app)
def test_get_override_single_dep(fastapi_dep):
with fastapi_dep(app).override({common_parameters: lambda: {"my": "override"}}):
response = client.get("/items/")
assert response.status_code == 200
assert response.json() == {"my": "override"}
You can reset your overrides by setting app.dependency_overrides to be an empty dict:
app.dependency_overrides = {}
According to official documentation: https://fastapi.tiangolo.com/advanced/testing-dependencies/
If you want to override a dependency only during some tests, you can set the override at the beginning of the test (inside the test function) and reset it at the end (at the end of the test function).
Based on this, we can conclude that the author of the framework does not see any danger in this.
But user Vlad is right. You may have a dependency common to several tests and a dependency used only in one test. Having dropped all dependencies in one test, you will not pass the subsequent ones that use common dependencies. This can be attributed to inconveniences.
Therefore, in my opinion, for convenience, it is better to use some kind of auxiliary mechanism proposed here

Vue-apollo. Queries don't work return 400 Bad Request

I am building a frontend with Vue cli and backend with Django with Graphene. The mutation works fine but not the queries.
When I run the same query from GraphiQL, works fine.
Frontend
#vue/cli 4.1.2
vue-apollo 3.0.2
Backend
python 3.8
django 3.0.2
graphene-django 2.8.0
django-graphql-jwt 0.3.0
queries.js
import gql from 'graphql-tag'
export const ME_QUERY = gql`
query me {
me {
username
is_active
}
}
`
Home.vue
<script>
import { ME_QUERY } from '#/graphql/queries'
export default {
name: 'home',
async mounted () {
await this.$apollo
.query({
query: ME_QUERY
})
.then((result) => {
console.log(result)
})
.catch(({ graphQLErrors }) => {
graphQLErrors.map(({ message, locations, path }) => console.log(`Error Message: ${message}, Location: ${locations}, Path: ${path}`))
})
}
}
</script>
schema.py
from django.contrib.auth import authenticate, login, get_user_model
import graphene
from graphene_django import DjangoObjectType
import graphql_jwt
from graphql_jwt.decorators import jwt_cookie
class UserType(DjangoObjectType):
class Meta:
model = get_user_model()
class ObtainJSONWebToken(graphql_jwt.JSONWebTokenMutation):
user = graphene.Field(UserType)
#classmethod
def resolve(cls, root, info, **kwargs):
return cls(user=info.context.user)
class Query(graphene.ObjectType):
me = graphene.Field(UserType)
def resolve_me(root, info):
user = info.context.user
if user.is_anonymous:
raise Exception('Authentication failure!!')
return user
class Mutation(graphene.ObjectType):
# token_auth = graphql_jwt.ObtainJSONWebToken.Field()
verify_token = graphql_jwt.Verify.Field()
refresh_token = graphql_jwt.Refresh.Field()
revoke_token = graphql_jwt.Revoke.Field()
log_in = ObtainJSONWebToken.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Thanks for advance
after to install the extension Graphql in Firefox and found my mistake.
{
"errors": [
{
"message": "Cannot query field \"is_active\" on type \"UserType\". Did you mean \"isActive\"?",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}
Changing to isActive the query worked fine.

The "override_settings" in TestCase is not work

I used "override_settings" in my test case, and I want to test DEFAULT_THROTTLE_RATES of REST_FRAMEWORK in the test case. When I tested my api, it didn't work.
my settings.py:
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'anon': '60/min',
'user': '500/hour',
'custom': '200/day',
}
}
test api:
#throttle_classes([AnonRateThrottle, UserRateThrottle])
def test_api(request):
pass
TestCase:
#override_settings(REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'anon': '600000/min',
'user': '5000000/hour',
'custom': '200000/day',
}
})
def test_api(self):
from rest_framework.settings import api_settings
print(api_settings.DEFAULT_THROTTLE_RATES)
print(api_settings.user_settings)
from rest_framework.throttling import AnonRateThrottle, api_settings as throttling_setting
print(AnonRateThrottle().get_rate())
print(id(throttling_setting))
print(id(api_settings))
print(id(AnonRateThrottle().THROTTLE_RATES))
print(id(api_settings.DEFAULT_THROTTLE_RATES))
print(id(throttling_setting.DEFAULT_THROTTLE_RATES))
url = 'api'
for i in range(100000):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
I want the test case to work, but it asserts in the state of 429.
I print the id of "apisetting", "DEFAULT_THROTTLE_RATES" , which is the same, but the "THROTTLE_RATES" id in the "AnonRateThrottle" instance is not the same, the value is not the same. The value is "{'anon': '60/min', 'user' : '500/hour', 'custom': '200/day',}". Please help me, I am going crazy.
The throttle of drf is worked by cache. I clear the cache with every request to make it work.

How can I rewrite one value in the settings for the test

How can I rewrite 'user': '5/minute' on 'user': '2/day' in the settings for the test
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'user': '5/minute'
}
}
How can i do this with #override_settings or mb with context manage?
I need to apply this only in one test, I can not rewrite the whole dictionary
def test_mytest(self):
value = settings.REST_FRAMEWORK
value['DEFAULT_THROTTLE_RATES'] = {'user':'2/day'}
data1 = {}
data2 = {}
data3 = {}
with self.settings(REST_FRAMEWORK=value):
resp1 = self.client.post(self.url, data1, format='json')
resp2 = self.client.post(self.url, data1, format='json')
resp3 = self.client.post(self.url, data1, format='json')
assert resp3.status_code == 429, resp3.data
But no have 429 error, although the value has changed
from unittest import mock
#mock.patch('rest_framework.throttling.SimpleRateThrottle.get_rate')
def test_api(self, get_rate):
get_rate.return_value = '1/minute'
from rest_framework.settings import api_settings
print(api_settings.DEFAULT_THROTTLE_RATES)
print(api_settings.user_settings)
url = 'api'
response = self.client.get(url)
self.assertEqual(response.status_code, HTTP_200_OK)
response = self.client.get(url)
self.assertEqual(response.status_code, HTTP_429_TOO_MANY_REQUESTS)
Solution 1: With the #override_default() wrapper
You want to apply a wrapper function to the viewset that you wish to override with this value:
from rest_framework.throttling import UserRateThrottle
class UserViewSet(viewsets.ViewSet):
#api_view(['PUT'])
#throttle_classes([UserRateThrottle])
#override_settings(REST_FRAMEWORK['DEFAULT_THROTTLE_RATES']['user'] = "2/day")
def update(self, request, pk=None):
...
Add the wrapper before any view or viewset you wish to apply this overridden customs value.
You also want this in your settings.py:
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
),
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '13500/day'
}
}
Solution 2: Custom throttling class
However, if you want different throttling rates for when you're in a test environment, perhaps try the following in settings.py:
TEST = True ## <-- Set to False when not in testing!!
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.CustomUserRateThrottle'
)
}
And designate your own custom throttling class:
from django.conf import settings
class CustomUserRateThrottle(throttling.UserRateThrottle):
if settings.TEST:
THROTTLE_RATES = 'DEFAULT_THROTTLE_RATES': {
'user': '13500/day',
}
else:
THROTTLE_RATES = 'DEFAULT_THROTTLE_RATES': {
'user': '2/day',
}
return settings.TEST # <-- Custom throttling classes must always return either True or False, so this makes sense.
Solution 3: directly in your "testing shell":
Include your custom wrapper as we defined above but this time in your test_something() method in test.py:
from django.conf import settings
from django.test import TestCase
class TestCase(TestCase):
def test_something(self):
with self.settings(REST_FRAMEWORK = ['DEFAULT_THROTTLE_RATES']['user'] = '13500/day'):
# START TESTING HERE WITH TEST SETTINGS
Depending on your use case you can follow one of the following ways :
Suggestion 1 : If you want to override settings in few specific test cases the you can use #override_settings decorator. But this make no sense if you want that for all test cases, if that's the case then you can follow suggestion 2.
Suggestion 2 : This is more generic and efficient solution.
You can have several settings file for various running environment (inside settings python module) something like this:
base.py (containing all of your basic settings)
local.py (import * from base here and override/add settings specific to local/dev environment)
test.py (import * from base here and override/add settings specific to test environment)
you can also have a settings specific for production environment in prod.py or something.
With this setup can run tests like python manage.py test --settings=yourproject.settings.test

python - Flask test_client() doesn't have request.authorization with pytest

I have problem when testing my flask app with pytest.
App is required basic auth which is parameters of request.authorization in flask.
But with pytest, flask.test_client() doesn't have request.authorization.
Here's a code of fixture:
#pytest.yield_fixture(scope='session')
def app()
app = create_app()
# some setup code
ctx = app.app_context()
ctx.push()
yield app
ctx.pop()
# some teadown code
#pytest.fixture
def test_client(app)
return app.test_client()
Here's a code of test:
def test_index(test_client):
res = test_client.get("/", headers={"Authorization": "Basic {user}".format(user=b64encode(b"test_user"))})
assert res.status_code == 200
When I run this test, I got this error:
E assert 401 == 200
E + where 401 = <Response streamed [401 UNAUTHORIZED]>.status_code
Not only auth failure, but also request.authorization doesn't have any value(None).
Why this happen? Is there any solution?
Thanks.
The credentials for HTTP Basic authentication must have a username and a password separated by a colon. If you're still using python 2, try this:
def test_index(test_client):
credentials = b64encode(b"test_user:test_password")
res = test_client.get("/", headers={"Authorization": "Basic {}".format(credentials)})
assert res.status_code == 200
Python 3 is a little stricter about data sanity, so you have to make sure that the bytes are properly decoded before sending them to the server:
def test_index(test_client):
credentials = b64encode(b"test_user:test_password").decode('utf-8')
res = test_client.get("/", headers={"Authorization": f"Basic {credentials}"})
assert res.status_code == 200
I found this solution. Maybe it can help someone:
from requests.auth import _basic_auth_str
headers = {
'Authorization': _basic_auth_str(username, password),
}
You just have to use the library 'requests'
from requests.auth import _basic_auth_str
headers = {
'Authorization': _basic_auth_str(username, password)
}
This works for me on both python 3.6 and 2.7 whereas the following only works for me on 2.7:
res = test_client.get("/", headers={"Authorization": "Basic {user}".format(user=b64encode(b"test_user:test_password"))})
If you are using new version of python (in my case 3.7) you should decode base64 string. It returns bytes and after stringify it looks like
b'basestring' which is not correct.
>>> base64.b64encode(b"user:password")
b'dXNlcjpwYXNzd29yZA=='
>>> base64.b64encode(b"user:password").decode()
'dXNlcjpwYXNzd29yZA=='
So, now my tests look like
class TestServer(unittest.TestCase):
def setUp(self) -> None:
self.client = app.test_client()
user_credentials = base64.b64encode(b"user:password").decode()
self.headers = {"Authorization": "Basic {}".format(user_credentials)}
Here is how I have wrote unit tests for API's that require authentication with custom token.
###### In your conftest.py file have the below methods
from connexion import FlaskApp
logging.basicConfig(level=logging.DEBUG)
API_FOLDER = pathlib.Path(__file__).parent / '..'
#pytest.fixture(scope="session")
def insecure_client(): # This is used for route tests that DO NOT require authorization.
cxn_app = FlaskApp(__name__,
port=5001,
specification_dir=API_FOLDER,
debug=True,
options={"debug": True, "swagger_ui": False})
cxn_app.add_api('your_api.yaml', resolver=RestyPlusResolver('api.routes'))
cxn_app._spec_file = 'your_api.yaml'
# connection stores the Flask app at app
cxn_app.app.config['SOME_KEY'] = config.CONFIG['SOME_KEY']
flask_jwt.JWT(cxn_app.app, None, None)
flask_cors.CORS(cxn_app.app)
cxn_app.app.app_context()
return cxn_app.app.test_client()
#pytest.fixture(scope="session")
def secure_client(): # This is used for route tests that REQUIRE authorization.
cxn_app = FlaskApp(__name__,
port=5001,
specification_dir=API_FOLDER,
debug=True,
options={"debug": True, "swagger_ui": False})
cxn_app.add_api('open_api.yaml', resolver=RestyPlusResolver('api.routes'))
cxn_app._spec_file = 'openapi.yaml'
# connection stores the Flask app at app
cxn_app.app.config['SOME_KEY'] = config.CONFIG['SOME_KEY']
flask_jwt.JWT(cxn_app.app, None, None)
flask_cors.CORS(cxn_app.app)
cxn_app.app.app_context()
client = cxn_app.app.test_client()
json_dict = {'user': 'your_username', 'password': 'your_pwd'}
# call the auth to get a token which can be used for API calls that require authentication.
# see below on how this is used in pytest of a route.
response = client.post('/auth', data=json.dumps(json_dict), content_type='application/json')
data = json_of_response(response)
setattr(client, '__token', data['token'])
return client
def post_json(client, url, json_dict):
"""Send dictionary json_dict as a json to the specified url """
return client.post(url, data=json.dumps(json_dict), content_type='application/json')
def json_of_response(response):
"""Decode json from response"""
return json.loads(response.data.decode('utf8'))
### Example Pytest of API that requires authentication.
def test_my_post(mocker, secure_client):
json_dict = {'id': 'TEST_01', 'phone': 'PHONE_02'}
mocker.patch('yourapi.services.User.create_user', return_value=("Success", 201))
response = secure_client.post('/user', data=json.dumps(json_dict), content_type='application/json', headers={'X-Auth':secure_client.__token})
data = json_of_response(response)
assert response.status_code == 201
assert data == "Success"