Testing¶
Having integrated unit tests that cover your API’s behavior is important, as it helps provide verification that your API code is still valid & working correctly with the rest of your application.
Tastypie provides some basic facilities that build on top of Django’s testing
support, in the form of a specialized TestApiClient
& ResourceTestCaseMixin
.
The ResourceTestCaseMixin
can be used along with Django’s TestCase
or other
Django test classes. It provides quite a few extra assertion methods that are specific
to APIs. Under the hood, it uses the TestApiClient
to perform requests properly.
The TestApiClient
builds on & exposes an interface similar to that of Django’s
Client
. However, under the hood, it hands all the setup needed to construct
a proper request.
Example Usage¶
The typical use case will primarily consist of adding the ResourceTestCaseMixin
class to an ordinary Django test class & using the built-in assertions to ensure your
API is behaving correctly. For the purposes of this example, we’ll assume the
resource in question looks like:
from tastypie.authentication import BasicAuthentication
from tastypie.resources import ModelResource
from entries.models import Entry
class EntryResource(ModelResource):
class Meta:
queryset = Entry.objects.all()
authentication = BasicAuthentication()
An example usage might look like:
import datetime
from django.contrib.auth.models import User
from django.test import TestCase
from tastypie.test import ResourceTestCaseMixin
from entries.models import Entry
class EntryResourceTest(ResourceTestCaseMixin, TestCase):
# Use ``fixtures`` & ``urls`` as normal. See Django's ``TestCase``
# documentation for the gory details.
fixtures = ['test_entries.json']
def setUp(self):
super(EntryResourceTest, self).setUp()
# Create a user.
self.username = 'daniel'
self.password = 'pass'
self.user = User.objects.create_user(self.username, 'daniel@example.com', self.password)
# Fetch the ``Entry`` object we'll use in testing.
# Note that we aren't using PKs because they can change depending
# on what other tests are running.
self.entry_1 = Entry.objects.get(slug='first-post')
# We also build a detail URI, since we will be using it all over.
# DRY, baby. DRY.
self.detail_url = '/api/v1/entry/{0}/'.format(self.entry_1.pk)
# The data we'll send on POST requests. Again, because we'll use it
# frequently (enough).
self.post_data = {
'user': '/api/v1/user/{0}/'.format(self.user.pk),
'title': 'Second Post!',
'slug': 'second-post',
'created': '2012-05-01T22:05:12'
}
def get_credentials(self):
return self.create_basic(username=self.username, password=self.password)
def test_get_list_unauthenticated(self):
self.assertHttpUnauthorized(self.api_client.get('/api/v1/entries/', format='json'))
def test_get_list_json(self):
resp = self.api_client.get('/api/v1/entries/', format='json', authentication=self.get_credentials())
self.assertValidJSONResponse(resp)
# Scope out the data for correctness.
self.assertEqual(len(self.deserialize(resp)['objects']), 12)
# Here, we're checking an entire structure for the expected data.
self.assertEqual(self.deserialize(resp)['objects'][0], {
'pk': str(self.entry_1.pk),
'user': '/api/v1/user/{0}/'.format(self.user.pk),
'title': 'First post',
'slug': 'first-post',
'created': '2012-05-01T19:13:42',
'resource_uri': '/api/v1/entry/{0}/'.format(self.entry_1.pk)
})
def test_get_list_xml(self):
self.assertValidXMLResponse(self.api_client.get('/api/v1/entries/', format='xml', authentication=self.get_credentials()))
def test_get_detail_unauthenticated(self):
self.assertHttpUnauthorized(self.api_client.get(self.detail_url, format='json'))
def test_get_detail_json(self):
resp = self.api_client.get(self.detail_url, format='json', authentication=self.get_credentials())
self.assertValidJSONResponse(resp)
# We use ``assertKeys`` here to just verify the keys, not all the data.
self.assertKeys(self.deserialize(resp), ['created', 'slug', 'title', 'user'])
self.assertEqual(self.deserialize(resp)['name'], 'First post')
def test_get_detail_xml(self):
self.assertValidXMLResponse(self.api_client.get(self.detail_url, format='xml', authentication=self.get_credentials()))
def test_post_list_unauthenticated(self):
self.assertHttpUnauthorized(self.api_client.post('/api/v1/entries/', format='json', data=self.post_data))
def test_post_list(self):
# Check how many are there first.
self.assertEqual(Entry.objects.count(), 5)
self.assertHttpCreated(self.api_client.post('/api/v1/entries/', format='json', data=self.post_data, authentication=self.get_credentials()))
# Verify a new one has been added.
self.assertEqual(Entry.objects.count(), 6)
def test_put_detail_unauthenticated(self):
self.assertHttpUnauthorized(self.api_client.put(self.detail_url, format='json', data={}))
def test_put_detail(self):
# Grab the current data & modify it slightly.
original_data = self.deserialize(self.api_client.get(self.detail_url, format='json', authentication=self.get_credentials()))
new_data = original_data.copy()
new_data['title'] = 'Updated: First Post'
new_data['created'] = '2012-05-01T20:06:12'
self.assertEqual(Entry.objects.count(), 5)
self.assertHttpAccepted(self.api_client.put(self.detail_url, format='json', data=new_data, authentication=self.get_credentials()))
# Make sure the count hasn't changed & we did an update.
self.assertEqual(Entry.objects.count(), 5)
# Check for updated data.
self.assertEqual(Entry.objects.get(pk=25).title, 'Updated: First Post')
self.assertEqual(Entry.objects.get(pk=25).slug, 'first-post')
self.assertEqual(Entry.objects.get(pk=25).created, datetime.datetime(2012, 3, 1, 13, 6, 12))
def test_delete_detail_unauthenticated(self):
self.assertHttpUnauthorized(self.api_client.delete(self.detail_url, format='json'))
def test_delete_detail(self):
self.assertEqual(Entry.objects.count(), 5)
self.assertHttpAccepted(self.api_client.delete(self.detail_url, format='json', authentication=self.get_credentials()))
self.assertEqual(Entry.objects.count(), 4)
Note that this example doesn’t cover other cases, such as filtering, PUT
to
a list endpoint, DELETE
to a list endpoint, PATCH
support, etc.
ResourceTestCaseMixin
API Reference¶
The ResourceTestCaseMixin
exposes the following methods for use. Most are
enhanced assertions or provide API-specific behaviors.
get_credentials
¶
- ResourceTestCaseMixin.get_credentials(self)¶
A convenience method for the user as a way to shorten up the often repetitious calls to create the same authentication.
Raises NotImplementedError
by default.
Usage:
class MyResourceTestCase(ResourceTestCaseMixin, TestCase):
def get_credentials(self):
return self.create_basic('daniel', 'pass')
# Then the usual tests...
create_basic
¶
- ResourceTestCaseMixin.create_basic(self, username, password)¶
Creates & returns the HTTP Authorization
header for use with BASIC Auth.
create_apikey
¶
- ResourceTestCaseMixin.create_apikey(self, username, api_key)¶
Creates & returns the HTTP Authorization
header for use with ApiKeyAuthentication
.
create_digest
¶
- ResourceTestCaseMixin.create_digest(self, username, api_key, method, uri)¶
Creates & returns the HTTP Authorization
header for use with Digest Auth.
create_oauth
¶
- ResourceTestCaseMixin.create_oauth(self, user)¶
Creates & returns the HTTP Authorization
header for use with Oauth.
assertHttpOK
¶
- ResourceTestCaseMixin.assertHttpOK(self, resp)¶
Ensures the response is returning a HTTP 200.
assertHttpCreated
¶
- ResourceTestCaseMixin.assertHttpCreated(self, resp)¶
Ensures the response is returning a HTTP 201.
assertHttpAccepted
¶
- ResourceTestCaseMixin.assertHttpAccepted(self, resp)¶
Ensures the response is returning either a HTTP 202 or a HTTP 204.
assertHttpMultipleChoices
¶
- ResourceTestCaseMixin.assertHttpMultipleChoices(self, resp)¶
Ensures the response is returning a HTTP 300.
assertHttpSeeOther
¶
- ResourceTestCaseMixin.assertHttpSeeOther(self, resp)¶
Ensures the response is returning a HTTP 303.
assertHttpNotModified
¶
- ResourceTestCaseMixin.assertHttpNotModified(self, resp)¶
Ensures the response is returning a HTTP 304.
assertHttpBadRequest
¶
- ResourceTestCaseMixin.assertHttpBadRequest(self, resp)¶
Ensures the response is returning a HTTP 400.
assertHttpForbidden
¶
- ResourceTestCaseMixin.assertHttpForbidden(self, resp)¶
Ensures the response is returning a HTTP 403.
assertHttpNotFound
¶
- ResourceTestCaseMixin.assertHttpNotFound(self, resp)¶
Ensures the response is returning a HTTP 404.
assertHttpMethodNotAllowed
¶
- ResourceTestCaseMixin.assertHttpMethodNotAllowed(self, resp)¶
Ensures the response is returning a HTTP 405.
assertHttpConflict
¶
- ResourceTestCaseMixin.assertHttpConflict(self, resp)¶
Ensures the response is returning a HTTP 409.
assertHttpGone
¶
- ResourceTestCaseMixin.assertHttpGone(self, resp)¶
Ensures the response is returning a HTTP 410.
assertHttpTooManyRequests
¶
- ResourceTestCaseMixin.assertHttpTooManyRequests(self, resp)¶
Ensures the response is returning a HTTP 429.
assertHttpApplicationError
¶
- ResourceTestCaseMixin.assertHttpApplicationError(self, resp)¶
Ensures the response is returning a HTTP 500.
assertHttpNotImplemented
¶
- ResourceTestCaseMixin.assertHttpNotImplemented(self, resp)¶
Ensures the response is returning a HTTP 501.
assertValidJSON
¶
- ResourceTestCaseMixin.assertValidJSON(self, data)¶
Given the provided data
as a string, ensures that it is valid JSON &
can be loaded properly.
assertValidXML
¶
- ResourceTestCaseMixin.assertValidXML(self, data)¶
Given the provided data
as a string, ensures that it is valid XML &
can be loaded properly.
assertValidYAML
¶
- ResourceTestCaseMixin.assertValidYAML(self, data)¶
Given the provided data
as a string, ensures that it is valid YAML &
can be loaded properly.
assertValidPlist
¶
- ResourceTestCaseMixin.assertValidPlist(self, data)¶
Given the provided data
as a string, ensures that it is valid binary plist &
can be loaded properly.
assertValidJSONResponse
¶
- ResourceTestCaseMixin.assertValidJSONResponse(self, resp)¶
Given a HttpResponse
coming back from using the client
, assert that
you get back:
An HTTP 200
The correct content-type (
application/json
)The content is valid JSON
assertValidXMLResponse
¶
- ResourceTestCaseMixin.assertValidXMLResponse(self, resp)¶
Given a HttpResponse
coming back from using the client
, assert that
you get back:
An HTTP 200
The correct content-type (
application/xml
)The content is valid XML
assertValidYAMLResponse
¶
- ResourceTestCaseMixin.assertValidYAMLResponse(self, resp)¶
Given a HttpResponse
coming back from using the client
, assert that
you get back:
An HTTP 200
The correct content-type (
text/yaml
)The content is valid YAML
assertValidPlistResponse
¶
- ResourceTestCaseMixin.assertValidPlistResponse(self, resp)¶
Given a HttpResponse
coming back from using the client
, assert that
you get back:
An HTTP 200
The correct content-type (
application/x-plist
)The content is valid binary plist data
deserialize
¶
- ResourceTestCaseMixin.deserialize(self, resp)¶
Given a HttpResponse
coming back from using the client
, this method
checks the Content-Type
header & attempts to deserialize the data based on
that.
It returns a Python datastructure (typically a dict
) of the serialized data.
serialize
¶
- ResourceTestCaseMixin.serialize(self, data, format='application/json')¶
Given a Python datastructure (typically a dict
) & a desired content-type,
this method will return a serialized string of that data.
assertKeys
¶
- ResourceTestCaseMixin.assertKeys(self, data, expected)¶
This method ensures that the keys of the data
match up to the keys of
expected
.
It covers the (extremely) common case where you want to make sure the keys of a response match up to what is expected. This is typically less fragile than testing the full structure, which can be prone to data changes.
ResourceTestCase
API Reference¶
ResourceTestCase
is deprecated and will be removed by v1.0.0.
class MyTest(ResourceTestCase)
is equivalent to
class MyTest(ResourceTestCaseMixin, TestCase)
.
TestApiClient
API Reference¶
The TestApiClient
simulates a HTTP client making calls to the API. It’s
important to note that it uses Django’s testing infrastructure, so it’s not
making actual calls against a webserver.
__init__
¶
- TestApiClient.__init__(self, serializer=None)¶
Sets up a fresh TestApiClient
instance.
If you are employing a custom serializer, you can pass the class to the
serializer=
kwarg.
get_content_type
¶
- TestApiClient.get_content_type(self, short_format)¶
Given a short name (such as json
or xml
), returns the full content-type
for it (application/json
or application/xml
in this case).
get
¶
- TestApiClient.get(self, uri, format='json', data=None, authentication=None, **kwargs)¶
Performs a simulated GET
request to the provided URI.
Optionally accepts a data
kwarg, which in the case of GET
, lets you
send along GET
parameters. This is useful when testing filtering or other
things that read off the GET
params. Example:
from tastypie.test import TestApiClient
client = TestApiClient()
response = client.get('/api/v1/entry/1/', data={'format': 'json', 'title__startswith': 'a', 'limit': 20, 'offset': 60})
Optionally accepts an authentication
kwarg, which should be an HTTP header
with the correct authentication data already setup.
All other **kwargs
passed in get passed through to the Django
TestClient
. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
for details.
post
¶
- TestApiClient.post(self, uri, format='json', data=None, authentication=None, **kwargs)¶
Performs a simulated POST
request to the provided URI.
Optionally accepts a data
kwarg. Unlike GET
, in POST
the
data
gets serialized & sent as the body instead of becoming part of the URI.
Example:
from tastypie.test import TestApiClient
client = TestApiClient()
response = client.post('/api/v1/entry/', data={
'created': '2012-05-01T20:02:36',
'slug': 'another-post',
'title': 'Another Post',
'user': '/api/v1/user/1/',
})
Optionally accepts an authentication
kwarg, which should be an HTTP header
with the correct authentication data already setup.
All other **kwargs
passed in get passed through to the Django
TestClient
. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
for details.
put
¶
- TestApiClient.put(self, uri, format='json', data=None, authentication=None, **kwargs)¶
Performs a simulated PUT
request to the provided URI.
Optionally accepts a data
kwarg. Unlike GET
, in PUT
the
data
gets serialized & sent as the body instead of becoming part of the URI.
Example:
from tastypie.test import TestApiClient
client = TestApiClient()
response = client.put('/api/v1/entry/1/', data={
'created': '2012-05-01T20:02:36',
'slug': 'another-post',
'title': 'Another Post',
'user': '/api/v1/user/1/',
})
Optionally accepts an authentication
kwarg, which should be an HTTP header
with the correct authentication data already setup.
All other **kwargs
passed in get passed through to the Django
TestClient
. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
for details.
patch
¶
- TestApiClient.patch(self, uri, format='json', data=None, authentication=None, **kwargs)¶
Performs a simulated PATCH
request to the provided URI.
Optionally accepts a data
kwarg. Unlike GET
, in PATCH
the
data
gets serialized & sent as the body instead of becoming part of the URI.
Example:
from tastypie.test import TestApiClient
client = TestApiClient()
response = client.patch('/api/v1/entry/1/', data={
'created': '2012-05-01T20:02:36',
'slug': 'another-post',
'title': 'Another Post',
'user': '/api/v1/user/1/',
})
Optionally accepts an authentication
kwarg, which should be an HTTP header
with the correct authentication data already setup.
All other **kwargs
passed in get passed through to the Django
TestClient
. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
for details.
delete
¶
- TestApiClient.delete(self, uri, format='json', data=None, authentication=None, **kwargs)¶
Performs a simulated DELETE
request to the provided URI.
Optionally accepts a data
kwarg, which in the case of DELETE
, lets you
send along DELETE
parameters. This is useful when testing filtering or other
things that read off the DELETE
params. Example:
from tastypie.test import TestApiClient
client = TestApiClient()
response = client.delete('/api/v1/entry/1/', data={'format': 'json'})
Optionally accepts an authentication
kwarg, which should be an HTTP header
with the correct authentication data already setup.
All other **kwargs
passed in get passed through to the Django
TestClient
. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
for details.