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.

assertHttpUnauthorized

ResourceTestCaseMixin.assertHttpUnauthorized(self, resp)

Ensures the response is returning a HTTP 401.

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.