What is the proper way of testing throttling in DRF? I coulnd't find out any answer to this question on the net. I want to have separate tests for each endpoint since each one has custom requests limits (ScopedRateThrottle).
The important thing is that it can't affect other tests - they have to somehow run without throttling and limiting.
An easy solution is to patch the get_rate method of your throttle class. Thanks to tprestegard for this comment!
I have a custom class in my case:
from rest_framework.throttling import UserRateThrottle
class AuthRateThrottle(UserRateThrottle):
scope = 'auth'
In your tests:
from unittest.mock import patch
from django.core.cache import cache
from rest_framework import status
class Tests(SimpleTestCase):
def setUp(self):
cache.clear()
#patch('path.to.AuthRateThrottle.get_rate')
def test_throttling(self, mock):
mock.return_value = '1/day'
response = self.client.post(self.url, {})
self.assertEqual(
response.status_code,
status.HTTP_400_BAD_REQUEST, # some fields are required
)
response = self.client.post(self.url, {})
self.assertEqual(
response.status_code,
status.HTTP_429_TOO_MANY_REQUESTS,
)
It is also possible to patch the method in the DRF package to change the behavior of the standard throttle classes: #patch('rest_framework.throttling.SimpleRateThrottle.get_rate')
Like people already mentioned, this doesn't exactly fall within the scope of unit tests, but still, how about simply doing something like this:
from django.core.urlresolvers import reverse
from django.test import override_settings
from rest_framework.test import APITestCase, APIClient
class ThrottleApiTests(APITestCase):
# make sure to override your settings for testing
TESTING_THRESHOLD = '5/min'
# THROTTLE_THRESHOLD is the variable that you set for DRF DEFAULT_THROTTLE_RATES
#override_settings(THROTTLE_THRESHOLD=TESTING_THRESHOLD)
def test_check_health(self):
client = APIClient()
# some end point you want to test (in this case it's a public enpoint that doesn't require authentication
_url = reverse('check-health')
# this is probably set in settings in you case
for i in range(0, self.TESTING_THRESHOLD):
client.get(_url)
# this call should err
response = client.get(_url)
# 429 - too many requests
self.assertEqual(response.status_code, 429)
Also, regarding your concerns of side-effects, as long as you do user creation in setUp or setUpTestData, tests will be isolated (as they should), so no need to worry about 'dirty' data or scope in that sense.
Regarding cache clearing between tests, I would just add cache.clear() in tearDown or try and clear the specific key defined for throttling.
I implemented my own caching mechanism for throttling based on the user and the parameters with which a request is called. You can override SimpleRateThrottle.get_cache_key to get this behavior.
Take this throttle class for example:
class YourCustomThrottleClass(SimpleRateThrottle):
rate = "1/d"
scope = "your-custom-throttle"
def get_cache_key(self, request: Request, view: viewsets.ModelViewSet):
# we want to throttle the based on the request user as well as the parameter
# `foo` (i.e. the user can make a request with a different `foo` as many times
# as they want in a day, but only once a day for a given `foo`).
foo_request_param = view.kwargs["foo"]
ident = f"{request.user.pk}_{foo_request_param}"
# below format is copied from `UserRateThrottle.get_cache_key`.
return self.cache_format % {"scope": self.scope, "ident": ident}
In order to clear this in a TestCase I call the following method in each test method as required:
def _clear_throttle_cache(self, request_user, foo_param):
# we need to clear the cache of the throttle limits already stored there.
throttle = YourCustomThrottleClass()
# in the below two lines mock whatever attributes on the request and
# view instances are used to calculate the cache key in `.get_cache_key`
# which you overrode. Here we use `request.user` and `view.kwargs["foo"]`
# to calculate the throttle key, so we mock those.
pretend_view = MagicMock(kwargs={foo: foo_param})
pretend_request = MagicMock(user=request_user)
# this is the method you overrode in `YourCustomThrottleClass`.
throttle_key = throttle.get_cache_key(pretend_request, pretend_view)
throttle.cache.delete(user_key)
This is an amendment on yofee's post which got me 90% there. When using a throttle, custom or otherwise, with a set rate, get_rate is never called. As shown below from the source.
def __init__(self):
if not getattr(self, 'rate', None):
self.rate = self.get_rate()
Hence when one is mocking a throttle with a set rate that is not None, I would recommend patching the rate attribute directly.
...
with mock.patch.object(AuthRateThrottle, 'rate', '1/day'):
...
Related
I have a Flask application using flask-restx and flask-login. I would like all routes by default to require login, and explicitly define public routes that require no authentication. I have started using decorators following the example given in this question:
Best way to make Flask-Login's login_required the default
It works for function endpoints, but not for restx resource endpoints.
I have tried adding the function both as a decorator, and using the method_decorators field. For example:
def public_route(decorated_function):
"""
This is a decorator to specify public endpoints in our flask routes
:param decorated_function:
:return:
"""
decorated_function.is_public = True
return decorated_function
class HelloWorld(ConfigurableResource):
method_decorators = {"get": [public_route]}
#public_route
#api.doc('Welcome message')
def get(self):
return {'hello': 'world'}
And this test passes:
def test_hello_world_is_public():
api = Namespace('health', description='Health related operations')
hello = HelloWorld(api, config=None, logger=None)
is_public_endpoint = getattr(hello.get, 'is_public', False)
assert is_public_endpoint
My challenge is I can't see how to access this attribute in my auth logic:
#app.before_request
def check_route_access():
"""
This function decides whethere access should be granted to an endpoint.
This function runs before all requests.
:return:
"""
is_public_endpoint = getattr(app.view_functions[request.endpoint], 'is_public', False)
if person_authorized_for_path(current_user, request.path, is_public_endpoint):
return
# Otherwise access not granted
return redirect(url_for("auth.index"))
This works for plain function endpoints, but not restx resources.
I understand that restx is wrapping my resource class in a function so that flask can do dispatch, but I can't figure out how to access the decorator from here. So I have some questions:
Is it possible to reach the decorator from the view_function?
Is it possible to know whether the endpoint is a restx resource or a plain rest function?
Is there a better way to do what I'm trying to achieve?
Based on this and this, method_decorators variable should be a list of functions, so you should use it like:
def _perform_auth(method):
is_public_endpoint = getattr(method, 'is_public', False)
# place the validation here
class Resource(flask_restx.Resource):
method_decorators = [_perform_auth]
Is it possible to reach the decorator from the view_function?
Well... it is possible, but I wouldn't recommend it. Here's an example
Is it possible to know whether the endpoint is a restx resource or a
plain rest function?
You probably can inspect the func and figure out if it's from restx, maybe looking at __qualname__, but then again, I`d wouldn't recommend it.
Is there a better way to do what I'm trying to achieve?
I would go one of these solutions:
Explicitly decorate view_funcs and resources that do need authentication, instead of the other way around
Create a blueprint for public endpoints, a blueprint for protected endpoints with the before_request decorator for authorization
I have a Django model that makes use of some libraries which I would like to be able to override. For instance, when testing I'd like to pass a mock instead of having my model tightly coupled. I can do this in python, but for the life of me I can't figure out how to do it with a Django model. Here's a simplified example not using Django:
import requests
class APIClient:
def __init__(self, **kwargs):
self.http_lib = kwargs.get("http_lib", requests)
def get_url(self, url):
return self.http_lib.get(url)
For regular use of this class I can still use requests but if I want to use a different library for some reason or if I want to test certain outcomes, I can invoke the class with client = APIClient(http_lib=MockRequests())
But how do I do that with a Django model? If I try to pass kwargs that aren't backed by a database field Django throws an error. Overriding __init__ is not considered a good practice either. Is there a way in Django to set and get a value that isn't backed by a database column?
Do you have a settings.TEST var? If so, you could make http_lib a function that returns the proper lib:
from django.conf import settings
def get_http_lib(mock=None):
if not mock:
return requests
return MockRequests()
class APIClient(Model):
def __init__(self, **kwargs):
# ...whatever...
#property
def some_column(self):
http_lib = get_http_lib(settings.TEST)
# ...etc...
Not ideal, but passable.
PRE-EDIT ANSWER (doesn't work):
What if you setattr subsequent to instantiating the Model?
# In model...
class APIClient(Model):
def __init__(self, **kwargs):
self.http_lib = requests
# ...etc...
# In tests...
client = APIClient()
setattr(client, 'http_lib', MockRequests())
I'm trying to reproduce an (obvious) problem in a ModelAdmin with Unittests.
In the ModelAdmin, I perform some extra operations when saving a model. In doing so, I introduce the new instance variable collection_page.
class MyModelAdmin(admin.ModelAdmin):
...
def save_model(self, requset, obj, form, change):
...
if obj_is_new:
self.collection_page = ....
...
self.collection_page # <== AttributeError if obj is not new.
I test the module using the django.test.Client twice, like this:
class CollectionAdminTestCase(django.test.TestCase):
...
def test_redirect_after_editing_existing_object(self):
self.client.post(
self.creation_path,
self.creation_post
)
response = self.client.post(
self.change_path,
self.change_post
)
self.assertEqual(
response.status_code, 302
)
The test passes, but it should fail.
When I add
try:
del self.collection_page
except AttributeError:
pass
at the beginning of the save_model method in MyModelAdmin, the test fails as it should.
Is this an intended feature of Django? Is there a better way to deal with it?
You should absolutely not be setting state on the ModelAdmin object. It lives for the lifetime of the server process; anything set on it will be preserved across requests.
You don't say what you are doing with this variable, so it's hard to give advice about how best to do whatever it is; only, not this way.
I am writing an Unit-Test for a Django class-based view.
class ExampleView(ListView):
def get_context_data(self, **kwargs):
context = super(EampleView, self).get_context_data(**kwargs)
## do something else
def get_queryset(self, **kwargs):
return self.get_data()
def get_data(self):
call_external_API()
## do something else
The key issue is that call_external_API() in get_data().
When I am writing Unit-test, I don't really want to call external API to get data. First, that will cost my money; second, I can easily test that API in another test file.
I also can easily test this get_data() method by having an unit-test only for it and mock the output of call_external_API().
However, when I test this whole class-based view, I simply will do
self.client.get('/example/url/')
and check the status code and context data to verify it.
In this case, how do I mock this call_external_API() when I am testing the whole class-based view?
What your are looking for is patch from unittest.mock. You can patch call_external_api() by a MagicMock() object.
Maybe you want to patch call_external_api() for all your tests in class. patch give to you essentialy two way to do it
decorate the test class
use start() and stop() in setUp() and tearDown() respectively
Decorate a class by patch decorator is like decorate all test methods (see documentation for details) and the implementation will be very neat. Follow example assume that your view is in my_view module.
#patch("my_view.call_external_api", autospec=True)
class MyTest(unittest.TestCase):
def setUp(self):
self.client = Client()
def test_get_data(self, mock_call_external_api):
self.client.get('/example/url/')
self.assertTrue(mock_call_external_api.called)
More sophisticate examples can be build and you can check how you call mock_call_external_api and set return value or side effects for your API.
I don't give any example about start and stop way to do it (I don't really like it) but I would like to spend some time on two details:
I assumed that in your my_view module you define call_external_api or you import it by from my_API_module import call_external_api otherwise you should pay some attention on Where to patch
I used autospec=True: IMHO it should be used in every patch call and documentation explain why very well
You can mock the call_external_api() method when testing the classed based view with something like this:
import modulea
import unittest
from mock import Mock
class ExampleTestCase(unittest.TestCase):
def setUp(self):
self.call_external_api = modulea.call_external_api
def tearDown(self):
modulea.call_external_api = self.call_external_api
def get_data(self):
modulea.call_external_api = Mock(return_value="foobar")
modulea.call_external_api()
## do something else
I have a view that should be setting an initial value for a form field based on a GET value. I want to test this. I'm currently using Django's test client but I am open to looking at other tools.
Edit
Sorry, I did not mention that I am well aware of the assertContains method but I was hoping there was a better way other than searching the HTML for an input tag and the value attribute.
Hate to answer my own question (like the 3rd time I've done it) but after mocking around with the test client, I've found a better way:
def test_creating_stop(self):
c = self.client
# Check that name is pre-filled
response = c.get('%s?name=abcd' % reverse('add_new_stop'))
self.assertEqual(response.context['form'].initial['name'], 'abcd')
Does anyone see anything wrong with this? I'll leave it up for a while see what people think.
The accepted solution check initial['...'] value on the form but you could also check the actual value on the field. Pseudo-code bellow.
This is helpful if you want to test a default value coming directly from the model (form.initial is not set) and to make sure that initial['...'] is the actual value.
def test_some_default_value(self):
response = self.client.get('/signup/')
self.assertEquals(response.context['form']['plan'].value(), my_value)
def test_some_default_value_2(self):
some_different_conditions...
response = self.client.get('/signup/')
self.assertEquals(response.context['form']['plan'].value(), a_different_value)
The value will be embedded in the html as <input value= 'whatever'/>. You can search for that string with whatever tool you prefer.
response = Client().get('/customer/details/')
print [line for line in response.split('\n') if line.find('<input') > -1]
I think this feature comes with 1.3 but it may have come in earlier. I've slightly modified the example on the page to work with your requirements, but it's untested code and I've assumed a few things like the form parameter of the response context. Modify as applicable. The point of this answer is to show the request factory.
http://docs.djangoproject.com/en/dev/topics/testing/#django.test.client.RequestFactory
from django.utils import unittest
from django.test.client import RequestFactory
class SimpleTest(unittest.TestCase):
def setUp(self):
# Every test needs access to the request factory.
self.factory = RequestFactory()
def test_details(self):
get_param = 'some_value'
# Create an instance of a GET request.
request = self.factory.get('/customer/details/?param={0}'.format(get_param))
# Test my_view() as if it were deployed at /customer/details
response = my_view(request)
# test 1
form = response.form
idx = form.as_p().find(get_param)
self.assertNotEqual(idx, -1)
#or.. test 2
self.assertContains(response, get_param)
If you strictly checking for the initial value of a form field, another alternative is testing your form:
forms.py:
from django import forms
class MyForm(forms.Form):
title = forms.CharField(initial='My Default Title')
test_forms.py
from django.test import TestCase
from .forms import MyForm
class MyFormTests(TestCase):
def test_myform_initial_value(self):
form = MyForm()
self.assertEqual(form['title'].initial, 'My Default Title')