mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-26 03:23:59 +03:00
Complete testing docs
This commit is contained in:
parent
d31d7c1867
commit
0a722de171
|
@ -217,6 +217,16 @@ Renders data into HTML for the Browsable API. This renderer will determine whic
|
|||
|
||||
**.charset**: `utf-8`
|
||||
|
||||
## MultiPartRenderer
|
||||
|
||||
This renderer is used for rendering HTML multipart form data. **It is not suitable as a response renderer**, but is instead used for creating test requests, using REST framework's [test client and test request factory][testing].
|
||||
|
||||
**.media_type**: `multipart/form-data; boundary=BoUnDaRyStRiNg`
|
||||
|
||||
**.format**: `'.multipart'`
|
||||
|
||||
**.charset**: `utf-8`
|
||||
|
||||
---
|
||||
|
||||
# Custom renderers
|
||||
|
@ -373,6 +383,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
|
|||
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
|
||||
[cors]: http://www.w3.org/TR/cors/
|
||||
[cors-docs]: ../topics/ajax-csrf-cors.md
|
||||
[testing]: testing.md
|
||||
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
|
||||
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||
[application/vnd.github+json]: http://developer.github.com/v3/media/
|
||||
|
|
|
@ -149,6 +149,33 @@ Default: `None`
|
|||
|
||||
---
|
||||
|
||||
## Test settings
|
||||
|
||||
*The following settings control the behavior of APIRequestFactory and APIClient*
|
||||
|
||||
#### TEST_REQUEST_DEFAULT_FORMAT
|
||||
|
||||
The default format that should be used when making test requests.
|
||||
|
||||
This should match up with the format of one of the renderer classes in the `TEST_REQUEST_RENDERER_CLASSES` setting.
|
||||
|
||||
Default: `'multipart'`
|
||||
|
||||
#### TEST_REQUEST_RENDERER_CLASSES
|
||||
|
||||
The renderer classes that are supported when building test requests.
|
||||
|
||||
The format of any of these renderer classes may be used when contructing a test request, for example: `client.post('/users', {'username': 'jamie'}, format='json')`
|
||||
|
||||
Default:
|
||||
|
||||
(
|
||||
'rest_framework.renderers.MultiPartRenderer',
|
||||
'rest_framework.renderers.JSONRenderer'
|
||||
)
|
||||
|
||||
---
|
||||
|
||||
## Browser overrides
|
||||
|
||||
*The following settings provide URL or form-based overrides of the default browser behavior.*
|
||||
|
|
|
@ -10,13 +10,100 @@ REST framework includes a few helper classes that extend Django's existing test
|
|||
|
||||
# APIRequestFactory
|
||||
|
||||
Extends Django's existing `RequestFactory`.
|
||||
Extends [Django's existing `RequestFactory` class][requestfactory].
|
||||
|
||||
**TODO**: Document making requests. Note difference on form PUT requests. Document configuration.
|
||||
## Creating test requests
|
||||
|
||||
The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available.
|
||||
|
||||
### Using the format arguments
|
||||
|
||||
Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example:
|
||||
|
||||
factory = APIRequestFactory()
|
||||
request = factory.post('/notes/', {'title': 'new idea'}, format='json')
|
||||
|
||||
By default the available formats are `'multipart'` and `'json'`. For compatibility with Django's existing `RequestFactory` the default format is `'multipart'`.
|
||||
|
||||
To support a wider set of request formats, or change the default format, [see the configuration section][configuration].
|
||||
|
||||
If you need to explictly encode the request body, you can do so by explicitly setting the `content_type` flag. For example:
|
||||
|
||||
request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json')
|
||||
|
||||
### PUT and PATCH with form data
|
||||
|
||||
One difference worth noting between Django's `RequestFactory` and REST framework's `APIRequestFactory` is that multipart form data will be encoded for methods other than just `.post()`.
|
||||
|
||||
For example, using `APIRequestFactory`, you can make a form PUT request like so:
|
||||
|
||||
factory = APIRequestFactory()
|
||||
request = factory.put('/notes/547/', {'title': 'remember to email dave'})
|
||||
|
||||
Using Django's `Factory`, you'd need to explicitly encode the data yourself:
|
||||
|
||||
factory = RequestFactory()
|
||||
data = {'title': 'remember to email dave'}
|
||||
content = encode_multipart('BoUnDaRyStRiNg', data)
|
||||
content_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
|
||||
request = factory.put('/notes/547/', content, content_type=content_type)
|
||||
|
||||
## Forcing authentication
|
||||
|
||||
When testing views directly using a request factory, it's often convenient to be able to directly authenticate the request, rather than having to construct the correct authentication credentials.
|
||||
|
||||
To forcibly authenticate a request, use the `force_authenticate()` method.
|
||||
|
||||
factory = APIRequestFactory()
|
||||
user = User.objects.get(username='olivia')
|
||||
view = AccountDetail.as_view()
|
||||
|
||||
# Make an authenticated request to the view...
|
||||
request = factory.get('/accounts/django-superstars/')
|
||||
force_authenticate(request, user=user)
|
||||
response = view(request)
|
||||
|
||||
The signature for the method is `force_authenticate(request, user=None, token=None)`. When making the call, either or both of the user and token may be set.
|
||||
|
||||
---
|
||||
|
||||
**Note**: When using `APIRequestFactory`, the object that is returned is Django's standard `HttpRequest`, and not REST framework's `Request` object, which is only generated once the view is called.
|
||||
|
||||
This means that setting attributes directly on the request object may not always have the effect you expect. For example, setting `.token` directly will have no effect, and setting `.user` directly will only work if session authentication is being used.
|
||||
|
||||
# Request will only authenticate if `SessionAuthentication` is in use.
|
||||
request = factory.get('/accounts/django-superstars/')
|
||||
request.user = user
|
||||
response = view(request)
|
||||
|
||||
---
|
||||
|
||||
## Forcing CSRF validation
|
||||
|
||||
By default, requests created with `APIRequestFactory` will not have CSRF validation applied when passed to a REST framework view. If you need to explicitly turn CSRF validation on, you can do so by setting the `enforce_csrf_checks` flag when instantiating the factory.
|
||||
|
||||
factory = APIRequestFactory(enforce_csrf_checks=True)
|
||||
|
||||
---
|
||||
|
||||
**Note**: It's worth noting that Django's standard `RequestFactory` doesn't need to include this option, because when using regular Django the CSRF validation takes place in middleware, which is not run when testing views directly. When using REST framework, CSRF validation takes place inside the view, so the request factory needs to disable view-level CSRF checks.
|
||||
|
||||
---
|
||||
|
||||
# APIClient
|
||||
|
||||
Extends Django's existing `Client`.
|
||||
Extends [Django's existing `Client` class][client].
|
||||
|
||||
## Making requests
|
||||
|
||||
The `APIClient` class supports the same request interface as `APIRequestFactory`. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example:
|
||||
|
||||
client = APIClient()
|
||||
client.post('/notes/', {'title': 'new idea'}, format='json')
|
||||
|
||||
To support a wider set of request formats, or change the default format, [see the configuration section][configuration].
|
||||
|
||||
## Authenticating
|
||||
|
||||
### .login(**kwargs)
|
||||
|
||||
|
@ -59,17 +146,23 @@ This can be a useful shortcut if you're testing the API but don't want to have t
|
|||
>>> client = APIClient()
|
||||
>>> client.force_authenticate(user=user)
|
||||
|
||||
To unauthenticate subsequant requests, call `force_authenticate` setting the user and/or token to `None`.
|
||||
To unauthenticate subsequent requests, call `force_authenticate` setting the user and/or token to `None`.
|
||||
|
||||
>>> client.force_authenticate(user=None)
|
||||
|
||||
### Making requests
|
||||
## CSRF validation
|
||||
|
||||
**TODO**: Document requests similarly to `APIRequestFactory`
|
||||
By default CSRF validation is not applied when using `APIClient`. If you need to explicitly enable CSRF validation, you can do so by setting the `enforce_csrf_checks` flag when instantiating the client.
|
||||
|
||||
client = APIClient(enforce_csrf_checks=True)
|
||||
|
||||
As usual CSRF validation will only apply to any session authenticated views. This means CSRF validation will only occur if the client has been logged in by calling `login()`.
|
||||
|
||||
---
|
||||
|
||||
# Testing responses
|
||||
|
||||
### Using request.data
|
||||
## Checking the response data
|
||||
|
||||
When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response.
|
||||
|
||||
|
@ -83,7 +176,7 @@ Instead of inspecting the result of parsing `request.content`:
|
|||
response = self.client.get('/users/4/')
|
||||
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})
|
||||
|
||||
### Rendering responses
|
||||
## Rendering responses
|
||||
|
||||
If you're testing views directly using `APIRequestFactory`, the responses that are returned will not yet be rendered, as rendering of template responses is performed by Django's internal request-response cycle. In order to access `response.content`, you'll first need to render the response.
|
||||
|
||||
|
@ -92,6 +185,36 @@ If you're testing views directly using `APIRequestFactory`, the responses that a
|
|||
response = view(request, pk='4')
|
||||
response.render() # Cannot access `response.content` without this.
|
||||
self.assertEqual(response.content, '{"username": "lauren", "id": 4}')
|
||||
|
||||
|
||||
[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
## Setting the default format
|
||||
|
||||
The default format used to make test requests may be set using the `TEST_REQUEST_DEFAULT_FORMAT` setting key. For example, to always use JSON for test requests by default instead of standard multipart form requests, set the following in your `settings.py` file:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
...
|
||||
'TEST_REQUEST_DEFAULT_FORMAT': 'json'
|
||||
}
|
||||
|
||||
## Setting the available formats
|
||||
|
||||
If you need to test requests using something other than multipart or json requests, you can do so by setting the `TEST_REQUEST_RENDERER_CLASSES` setting.
|
||||
|
||||
For example, to add support for using `format='yaml'` in test requests, you might have something like this in your `settings.py` file.
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
...
|
||||
'TEST_REQUEST_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.MultiPartRenderer',
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.YAMLRenderer'
|
||||
)
|
||||
}
|
||||
|
||||
[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper
|
||||
[client]: https://docs.djangoproject.com/en/dev/topics/testing/overview/#module-django.test.client
|
||||
[requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory
|
||||
[configuration]: #configuration
|
||||
|
|
|
@ -50,7 +50,7 @@ class Response(SimpleTemplateResponse):
|
|||
charset = renderer.charset
|
||||
content_type = self.content_type
|
||||
|
||||
if content_type is None and charset is not None and ';' not in media_type:
|
||||
if content_type is None and charset is not None:
|
||||
content_type = "{0}; charset={1}".format(media_type, charset)
|
||||
elif content_type is None:
|
||||
content_type = media_type
|
||||
|
|
|
@ -73,6 +73,13 @@ DEFAULTS = {
|
|||
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
|
||||
'UNAUTHENTICATED_TOKEN': None,
|
||||
|
||||
# Testing
|
||||
'TEST_REQUEST_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.MultiPartRenderer',
|
||||
'rest_framework.renderers.JSONRenderer'
|
||||
),
|
||||
'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',
|
||||
|
||||
# Browser enhancements
|
||||
'FORM_METHOD_OVERRIDE': '_method',
|
||||
'FORM_CONTENT_OVERRIDE': '_content',
|
||||
|
@ -115,6 +122,7 @@ IMPORT_STRINGS = (
|
|||
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
|
||||
'DEFAULT_FILTER_BACKENDS',
|
||||
'FILTER_BACKEND',
|
||||
'TEST_REQUEST_RENDERER_CLASSES',
|
||||
'UNAUTHENTICATED_USER',
|
||||
'UNAUTHENTICATED_TOKEN',
|
||||
)
|
||||
|
|
|
@ -1,22 +1,31 @@
|
|||
# -- coding: utf-8 --
|
||||
|
||||
# Note that we use `DjangoRequestFactory` and `DjangoClient` names in order
|
||||
# Note that we import as `DjangoRequestFactory` and `DjangoClient` in order
|
||||
# to make it harder for the user to import the wrong thing without realizing.
|
||||
from __future__ import unicode_literals
|
||||
from django.conf import settings
|
||||
from django.test.client import Client as DjangoClient
|
||||
from django.test.client import ClientHandler
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.compat import RequestFactory as DjangoRequestFactory
|
||||
from rest_framework.compat import force_bytes_or_smart_bytes, six
|
||||
from rest_framework.renderers import JSONRenderer, MultiPartRenderer
|
||||
|
||||
|
||||
def force_authenticate(request, user=None, token=None):
|
||||
request._force_auth_user = user
|
||||
request._force_auth_token = token
|
||||
|
||||
|
||||
class APIRequestFactory(DjangoRequestFactory):
|
||||
renderer_classes = {
|
||||
'json': JSONRenderer,
|
||||
'multipart': MultiPartRenderer
|
||||
}
|
||||
default_format = 'multipart'
|
||||
renderer_classes_list = api_settings.TEST_REQUEST_RENDERER_CLASSES
|
||||
default_format = api_settings.TEST_REQUEST_DEFAULT_FORMAT
|
||||
|
||||
def __init__(self, enforce_csrf_checks=False, **defaults):
|
||||
self.enforce_csrf_checks = enforce_csrf_checks
|
||||
self.renderer_classes = {}
|
||||
for cls in self.renderer_classes_list:
|
||||
self.renderer_classes[cls.format] = cls
|
||||
super(APIRequestFactory, self).__init__(**defaults)
|
||||
|
||||
def _encode_data(self, data, format=None, content_type=None):
|
||||
"""
|
||||
|
@ -35,18 +44,24 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
ret = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
|
||||
|
||||
else:
|
||||
# Use format and render the data into a bytestring
|
||||
format = format or self.default_format
|
||||
|
||||
assert format in self.renderer_classes, ("Invalid format '{0}'. "
|
||||
"Available formats are {1}. Set TEST_REQUEST_RENDERER_CLASSES "
|
||||
"to enable extra request formats.".format(
|
||||
format,
|
||||
', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()])
|
||||
)
|
||||
)
|
||||
|
||||
# Use format and render the data into a bytestring
|
||||
renderer = self.renderer_classes[format]()
|
||||
ret = renderer.render(data)
|
||||
|
||||
# Determine the content-type header from the renderer
|
||||
if ';' in renderer.media_type:
|
||||
content_type = renderer.media_type
|
||||
else:
|
||||
content_type = "{0}; charset={1}".format(
|
||||
renderer.media_type, renderer.charset
|
||||
)
|
||||
content_type = "{0}; charset={1}".format(
|
||||
renderer.media_type, renderer.charset
|
||||
)
|
||||
|
||||
# Coerce text to bytes if required.
|
||||
if isinstance(ret, six.text_type):
|
||||
|
@ -74,6 +89,11 @@ class APIRequestFactory(DjangoRequestFactory):
|
|||
data, content_type = self._encode_data(data, format, content_type)
|
||||
return self.generic('OPTIONS', path, data, content_type, **extra)
|
||||
|
||||
def request(self, **kwargs):
|
||||
request = super(APIRequestFactory, self).request(**kwargs)
|
||||
request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
|
||||
return request
|
||||
|
||||
|
||||
class ForceAuthClientHandler(ClientHandler):
|
||||
"""
|
||||
|
@ -82,25 +102,21 @@ class ForceAuthClientHandler(ClientHandler):
|
|||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._force_auth_user = None
|
||||
self._force_auth_token = None
|
||||
self._force_user = None
|
||||
self._force_token = None
|
||||
super(ForceAuthClientHandler, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_response(self, request):
|
||||
# This is the simplest place we can hook into to patch the
|
||||
# request object.
|
||||
request._force_auth_user = self._force_auth_user
|
||||
request._force_auth_token = self._force_auth_token
|
||||
force_authenticate(request, self._force_user, self._force_token)
|
||||
return super(ForceAuthClientHandler, self).get_response(request)
|
||||
|
||||
|
||||
class APIClient(APIRequestFactory, DjangoClient):
|
||||
def __init__(self, enforce_csrf_checks=False, **defaults):
|
||||
# Note that our super call skips Client.__init__
|
||||
# since we don't need to instantiate a regular ClientHandler
|
||||
super(DjangoClient, self).__init__(**defaults)
|
||||
super(APIClient, self).__init__(**defaults)
|
||||
self.handler = ForceAuthClientHandler(enforce_csrf_checks)
|
||||
self.exc_info = None
|
||||
self._credentials = {}
|
||||
|
||||
def credentials(self, **kwargs):
|
||||
|
@ -114,10 +130,10 @@ class APIClient(APIRequestFactory, DjangoClient):
|
|||
Forcibly authenticates outgoing requests with the given
|
||||
user and/or token.
|
||||
"""
|
||||
self.handler._force_auth_user = user
|
||||
self.handler._force_auth_token = token
|
||||
self.handler._force_user = user
|
||||
self.handler._force_token = token
|
||||
|
||||
def request(self, **request):
|
||||
def request(self, **kwargs):
|
||||
# Ensure that any credentials set get added to every request.
|
||||
request.update(self._credentials)
|
||||
return super(APIClient, self).request(**request)
|
||||
kwargs.update(self._credentials)
|
||||
return super(APIClient, self).request(**kwargs)
|
||||
|
|
|
@ -6,11 +6,11 @@ from django.test import TestCase
|
|||
from rest_framework.compat import patterns, url
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework.test import APIClient, APIRequestFactory, force_authenticate
|
||||
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
def mirror(request):
|
||||
def view(request):
|
||||
return Response({
|
||||
'auth': request.META.get('HTTP_AUTHORIZATION', b''),
|
||||
'user': request.user.username
|
||||
|
@ -18,11 +18,11 @@ def mirror(request):
|
|||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^view/$', mirror),
|
||||
url(r'^view/$', view),
|
||||
)
|
||||
|
||||
|
||||
class CheckTestClient(TestCase):
|
||||
class TestAPITestClient(TestCase):
|
||||
urls = 'rest_framework.tests.test_testing'
|
||||
|
||||
def setUp(self):
|
||||
|
@ -66,3 +66,50 @@ class CheckTestClient(TestCase):
|
|||
expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
|
||||
class TestAPIRequestFactory(TestCase):
|
||||
def test_csrf_exempt_by_default(self):
|
||||
"""
|
||||
By default, the test client is CSRF exempt.
|
||||
"""
|
||||
user = User.objects.create_user('example', 'example@example.com', 'password')
|
||||
factory = APIRequestFactory()
|
||||
request = factory.post('/view/')
|
||||
request.user = user
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_explicitly_enforce_csrf_checks(self):
|
||||
"""
|
||||
The test client can enforce CSRF checks.
|
||||
"""
|
||||
user = User.objects.create_user('example', 'example@example.com', 'password')
|
||||
factory = APIRequestFactory(enforce_csrf_checks=True)
|
||||
request = factory.post('/view/')
|
||||
request.user = user
|
||||
response = view(request)
|
||||
expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
def test_invalid_format(self):
|
||||
"""
|
||||
Attempting to use a format that is not configured will raise an
|
||||
assertion error.
|
||||
"""
|
||||
factory = APIRequestFactory()
|
||||
self.assertRaises(AssertionError, factory.post,
|
||||
path='/view/', data={'example': 1}, format='xml'
|
||||
)
|
||||
|
||||
def test_force_authenticate(self):
|
||||
"""
|
||||
Setting `force_authenticate()` forcibly authenticates the request.
|
||||
"""
|
||||
user = User.objects.create_user('example', 'example@example.com')
|
||||
factory = APIRequestFactory()
|
||||
request = factory.get('/view')
|
||||
force_authenticate(request, user=user)
|
||||
response = view(request)
|
||||
self.assertEqual(response.data['user'], 'example')
|
||||
|
|
Loading…
Reference in New Issue
Block a user