Permalink
Fetching contributors…
Cannot retrieve contributors at this time
604 lines (400 sloc) 19.6 KB

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

.. method:: 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

.. method:: ResourceTestCaseMixin.create_basic(self, username, password)

Creates & returns the HTTP Authorization header for use with BASIC Auth.

create_apikey

.. method:: ResourceTestCaseMixin.create_apikey(self, username, api_key)

Creates & returns the HTTP Authorization header for use with ApiKeyAuthentication.

create_digest

.. method:: ResourceTestCaseMixin.create_digest(self, username, api_key, method, uri)

Creates & returns the HTTP Authorization header for use with Digest Auth.

create_oauth

.. method:: ResourceTestCaseMixin.create_oauth(self, user)

Creates & returns the HTTP Authorization header for use with Oauth.

assertHttpOK

.. method:: ResourceTestCaseMixin.assertHttpOK(self, resp)

Ensures the response is returning a HTTP 200.

assertHttpCreated

.. method:: ResourceTestCaseMixin.assertHttpCreated(self, resp)

Ensures the response is returning a HTTP 201.

assertHttpAccepted

.. method:: ResourceTestCaseMixin.assertHttpAccepted(self, resp)

Ensures the response is returning either a HTTP 202 or a HTTP 204.

assertHttpMultipleChoices

.. method:: ResourceTestCaseMixin.assertHttpMultipleChoices(self, resp)

Ensures the response is returning a HTTP 300.

assertHttpSeeOther

.. method:: ResourceTestCaseMixin.assertHttpSeeOther(self, resp)

Ensures the response is returning a HTTP 303.

assertHttpNotModified

.. method:: ResourceTestCaseMixin.assertHttpNotModified(self, resp)

Ensures the response is returning a HTTP 304.

assertHttpBadRequest

.. method:: ResourceTestCaseMixin.assertHttpBadRequest(self, resp)

Ensures the response is returning a HTTP 400.

assertHttpUnauthorized

.. method:: ResourceTestCaseMixin.assertHttpUnauthorized(self, resp)

Ensures the response is returning a HTTP 401.

assertHttpForbidden

.. method:: ResourceTestCaseMixin.assertHttpForbidden(self, resp)

Ensures the response is returning a HTTP 403.

assertHttpNotFound

.. method:: ResourceTestCaseMixin.assertHttpNotFound(self, resp)

Ensures the response is returning a HTTP 404.

assertHttpMethodNotAllowed

.. method:: ResourceTestCaseMixin.assertHttpMethodNotAllowed(self, resp)

Ensures the response is returning a HTTP 405.

assertHttpConflict

.. method:: ResourceTestCaseMixin.assertHttpConflict(self, resp)

Ensures the response is returning a HTTP 409.

assertHttpGone

.. method:: ResourceTestCaseMixin.assertHttpGone(self, resp)

Ensures the response is returning a HTTP 410.

assertHttpTooManyRequests

.. method:: ResourceTestCaseMixin.assertHttpTooManyRequests(self, resp)

Ensures the response is returning a HTTP 429.

assertHttpApplicationError

.. method:: ResourceTestCaseMixin.assertHttpApplicationError(self, resp)

Ensures the response is returning a HTTP 500.

assertHttpNotImplemented

.. method:: ResourceTestCaseMixin.assertHttpNotImplemented(self, resp)

Ensures the response is returning a HTTP 501.

assertValidJSON

.. method:: ResourceTestCaseMixin.assertValidJSON(self, data)

Given the provided data as a string, ensures that it is valid JSON & can be loaded properly.

assertValidXML

.. method:: ResourceTestCaseMixin.assertValidXML(self, data)

Given the provided data as a string, ensures that it is valid XML & can be loaded properly.

assertValidYAML

.. method:: ResourceTestCaseMixin.assertValidYAML(self, data)

Given the provided data as a string, ensures that it is valid YAML & can be loaded properly.

assertValidPlist

.. method:: ResourceTestCaseMixin.assertValidPlist(self, data)

Given the provided data as a string, ensures that it is valid binary plist & can be loaded properly.

assertValidJSONResponse

.. method:: 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

.. method:: 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

.. method:: 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

.. method:: 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

.. method:: 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

.. method:: 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

.. method:: 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__

.. method:: 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

.. method:: 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

.. method:: 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

.. method:: 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

.. method:: 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

.. method:: 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

.. method:: 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.