django-skivvy

Testing views for Django involves a lot of repetitive code. Each test case evaluates similar cases:

  • Accessing the view with an authorized user and returning the correct response content and status.
  • Accessing the view with an unauthorized user and return the right response content and status.
  • Accessing the view with an unauthenticated user and redirecting to the login page.
  • Accessing an object, which is not found in the database and returning a 404 error.
  • Posting a valid payload.
  • Posting an invalid payload.

Many of these cases need to be set up in similar ways. The test client needs to call the URL with specified parameters, the user needs to be logged in, an expected response needs to be rendered with a given template and template context and evaluated.

Using the Django built-in test client is very slow. So we have been experimenting with alternative approaches to testing views. Our approach, however, involves even more repetitive boilerplate code. Views need to be initialized with parameters to identify objects in the database; users need to be assigned, templates need to be rendered with specific template contexts, etc., etc.

django-skivvy is a mini test framework that addresses the problems and that helps you write better and more readable tests for Django views. You can focus on parametrizing your tests, while django-skivvy takes care of running the tests.

Testing a view

django-skivvy provides a mixin ViewTestCase to add additional helper methods to your test case.

from django.test import TestCase
from skivvy import ViewTestCase

class MyViewTestCase(ViewTestCase, TestCase):
    # your tests here

Testing Django Rest Framework API views

To test instances of DRF's APIView, use APITestCase instead. It behaves exactly as ViewTestCase; the only difference are a few internals in the test setup.

from django.test import TestCase
from skivvy import APITestCase

class MyViewTestCase(APITestCase, TestCase):
    # your tests here

Getting a response

The method request returns a response from your view. The response returned is based on the generic configuration of the test case.

To test special cases, you can use url_kwargs, get_data, post_data, session_data, view_kwargs and content_type parameters to temporarily overwrite the generic test setup.

Argument Type Default Description
method str GET HTTP method used for the request
user User AnonymousUser User authenticated with this request.
url_kwargs dict {} URL arguments passed to the view. ViewTestCase applies this dictionary to what is defined in url_kwargs or get_url_kwargs .
get_data dict {} Adds query parameters to the request URL. E.g., to test a request to /some/path/?filter=foo add get_data={'filter': 'foo'} .
post_data dict {} Request payload, only relevant for POST , PUT and PATCH requests. ViewTestCase applies this dictionary to what is defined in post_data or setup_post_data . Partial overwrites are allowed.
session_data dict {} If your view relies on data from the session store, you can provide this data using session_data .
view_kwargs dict {} Overwrites attributes set in the view class. The behaviour corresponds to providing keyword arguments to a class-based view's as_view() method .
content_type str application/json Only available for APITestCase . Sets the content type encoding for the request.
class MyViewTestCase(ViewTestCase, TestCase):
    def test_a_view(self):
        response =  self.request(method='GET',
                                 user=AnonymousUser(),
                                 url_kwargs={},
                                 get_data={},
                                 post_data={},
                                 session_data={},
                                 view_kwargs={})

Evaluating a response

request does not return a Django HTTPResponse object. The returned object provides convenient access to important response properties:

Property Type Description
status_code int HTTP status code of the response
content str , dict Content of the response. None if request results in a redirect. If the response is of type application/json , the response will be parsed into a dict .
location str Redirect location. None if the request does not result in a redirect.
messages list A list of all messages added to the session.
headers dict Dictionary of response headers returned from the view.

Example use

class MyViewTestCase(ViewTestCase, TestCase):
    def test_a_view(self):
        response =  self.request(user=some_user)
        assert response.status_code == 200
        assert response.content == expected_content

Test configuration

django-skivvy's test configuration borrows many ideas from Django's generic views. All details of the test configuration can be either set by a constant instance attribute or by overwriting a method, which allows you to add more logic to the test setup.

Creating model instances

To create model instances that are required in your test, add the method setup_models to the test case.

class MyViewTestCase(ViewTestCase, TestCase):
    def setup_models(self):
        self.project = Project.objects.create(
            name='My Project'
        )

View class

Each test case should only test one view class. To configure the view class, you can use the view_class attribute or the setup_view method. One of both is required; if both view_class and setup_view provided, the method setup_view is preferred.

Attribute: view_class

from myapp.views import MyView

class MyViewTestCase(ViewTestCase, TestCase):
    view_class = MyView

Method: setup_view()

from myapp.views import MyView

class MyViewTestCase(ViewTestCase, TestCase):
    def setup_view(self):
        return MyView.as_view()

URL arguments

Many URL patterns expect certain keys, which are passed to connected views to identify model instances. These arguments need to be provided to the view in the test case. To configure URL arguments, you can set the attribute url_kwargs or implement the method setup_url_kwargs. If neither url_kwargs or setup_url_kwargs are present, an empty dict ({}) is passed to the view.

Both url_kwargs or setup_url_kwargs define default URL arguments for all tests in the test case. Sometimes you might want to test how the view behaves under varying conditions, for instance when you want to test that a 404 error is returned when a model instance cannot be found in the database. You can overwrite individual URL keys in the request method, by providing the optional url_kwargs argument:

If you have several URL arguments, and you want to overwrite only some of them, it's sufficient only to provide the keys you want to change. django-skivvy merges those with the default URL arguments.

Attribute: url_kwargs

from myapp.views import MyView

class MyViewTestCase(ViewTestCase, TestCase):
    url_kwargs = {
        'project_id': 'abc123'
    }

Method: setup_url_kwargs

class MyViewTestCase(ViewTestCase, TestCase):
    def setup_models(self):
        self.project = Project.objects.create(name='My Project')

    def setup_url_kwargs(self):
        return {
            'project_id': self.project.id
        }
from myapp.views import MyView

class MyViewTestCase(ViewTestCase, TestCase):
    url_kwargs = {
        'project_id': 'abc123'
    }

    def test_not_found(self):
        response = self.request(url_kwargs={'project_id': 'def456'})

URL query parameters

To query parameters to the request URL, you can set the get_data attribute or implement setup_get_data(). If neither get_data or setup_get_data() are present, no query parameters will be added.

To add a search query parameter

Both get_data or setup_get_data define default request query parameters for all tests in the test case. Sometimes you might want to test how the view behaves under varying conditions, for instance, if a resource list is filtered correctly. You can overwrite selected query parameters, by providing the optional get_data argument to the request.

Attribute: get_data

from myapp.views import MyView

class MyViewTestCase(ViewTestCase, TestCase):
    get_data = {
        'search': 'foo'
    }

method: setup_get_data

class MyViewTestCase(ViewTestCase, TestCase):
    def setup_models(self):
        self.project = Project.objects.create(name='My Project')

    def setup_get_data(self):
        return {
            'search': self.project.name
        }
class MyViewTestCase(ViewTestCase, TestCase):
    post_data = {
        'name': 'New name'
    }

    def test_invalid_post(self):
        response = self.request(method='GET', get_data={'filter': 'name'})

Request meta attributes

You can add additional meta attributes to the request by setting the request_meta attribute or implementing setup_request_meta().

Both request_meta or setup_request_meta define default request meta attributes for all tests in the test case. You can overwrite selected meta attributes, by providing the optional request_meta argument to the request.

Attribute: request_meta

from myapp.views import MyView

class MyViewTestCase(ViewTestCase, TestCase):
    request_meta = {
        'HTTP_REFERER': 'http://example.com'
    }

method: setup_post_data

class MyViewTestCase(ViewTestCase, TestCase):
    def setup_models(self):
        self.project = Project.objects.create(name='My Project')

    def setup_request_meta(self):
        return {
            'HTTP_REFERER': 'http://example.com/' + self.project.id
        }
class MyViewTestCase(ViewTestCase, TestCase):
    request_meta = {
        'HTTP_REFERER': 'http://example.com'
    }

    def test_invalid_referrer(self):
        response = self.request(request_meta={'HTTP_REFERER': 'http://example.com/not-allowed'})

Request payload

To setup a default request payload for POST, PATCH or PUT request, you can set the post_data attribute or implement setup_post_data(). If neither post_data or setup_post_data() are present, the request payload defaults to {}.

Both post_data or setup_post_data define default request payloads for all tests in the test case. Sometimes you might want to test how the view behaves under varying conditions, for instance, that an invalid payload is handled correctly. You can overwrite parts of the request payload in the request method, by providing the optional post_data argument:

Attribute: post_data

from myapp.views import MyView

class MyViewTestCase(ViewTestCase, TestCase):
    post_data = {
        'name': 'New name'
    }

method: setup_post_data

class MyViewTestCase(ViewTestCase, TestCase):
    def setup_models(self):
        self.project = Project.objects.create(name='My Project')

    def setup_post_data(self):
        return {
            'name': self.project.name + ' #1'
        }
class MyViewTestCase(ViewTestCase, TestCase):
    post_data = {
        'name': 'New name'
    }

    def test_invalid_post(self):
        response = self.request(method='POST', post_data={'name': 'Invalid name'})

Viewsets

If the view class you are testing is a ViewSet, then you have to configure viewset_actions in the test case.

Attribute viewset_actions

class MyViewTestCase(APITestCase, TestCase):
    view_class = ViewSetInstance
    viewset_actions = {'get': 'list'}

Evaluating response content

To evaluate the response's content, ViewTestCase provides the property expected_content that you can use in the test's assertions.

expected_content can be configured by providing a template name and a template context.

class MyViewTestCase(ViewTestCase, TestCase):
    def test_correct_content(self):
        response = self.request(method='GET')
        assert response.status_code == 200
        assert response.content == self.expected_content

Configuring template name

To configure the template name you can set the attribute template or implement the method setup_template of you need more logic. Either template or setup_template() are required; if both are present setup_template() will be prefered.

Attribute: template

class MyViewTestCase(ViewTestCase, TestCase):
    template = 'projects/project_detail.html'

method: setup_template

class MyViewTestCase(ViewTestCase, TestCase):
    def setup_template(self):
        return 'projects/project_detail.html'

Configuring template context

To configure the template context, you can provide a static context using the template_context or implement setup_template_context(). If neither template_context or setup_template_context() are present the template context used defaults to {}; if both are present setup_template_context() will be prefered.

Attribute: template_context
class MyViewTestCase(ViewTestCase, TestCase):
    template_context = {
        'object_id': 'abc123'
    }

Method: setup_template_context

class MyViewTestCase(ViewTestCase, TestCase):
    def setup_models(self):
        self.project = Project.objects.create(name='My Project')

    def setup_template_context(self):
        form = MyForm(instance=self.project)
        return {
            'object': self.project,
            'form': form
        }

Overwriting the template context

The combination of template and template context defines the default expected response for all tests in the test case. In some cases, you want to test if the template is rendered with an alternative context, for instance, if a form renders the error messages correctly. expected_content uses the method render_content internally. You can use the same method to render alternative views of the template by updating the default context with new values. render_content() allows to add an arbitrary number of keyword arguments, which update the default context.

class MyViewTestCase(ViewTestCase, TestCase):
    template = 'project_form.html'
    post_data = {
        'name': 'New name'
    }

    def setup_models(self):
        self.project = Project.objects.create(name='My Project')

    def setup_template_context(self):
        form = MyForm(instance=self.project)
        return {
            'object': self.project,
            'form': form
        }

    def test_invalid_post(self):
        invalid_data = {'name': 'invalid name'}
        invalid_form = MyForm(instance=self.project, data=invalid_data)
        expected_response = self.render_content(form=invalid_form)

        response = self.request(method='POST', post_data=invalid_data)
        assert response.status_code == 200
        assert response.content == expected_response

Removing CSRF tokens from the response

Since version 1.10, Django changes the CSRF token on each request. If you render a template twice the CSRF token changes and comparing both results will fail.

django-skivvy removes all CRSF tokens from rendered response automatically. If you have a special case where you render a template without using django-skivvy, you can use remove_csrf to remove the token from the response.

from skivvy import remove_csrf

class MyViewTestCase(ViewTestCase, TestCase):
    def test_view(self):
        response_html = self.get_some_rendered_response()
        no_csrf = remove_csrf(response_html)

Evaluating redirects

When a view redirects to a different location after a request, you can test that too. It is, for example, common to redirect to an object's detail page after an object is created or updated, or to redirect to the login page, when a login is required.

expected_success_url

ViewTestCase provides the property expected_success_url that you can use in the test's assertions.

There are different ways to configure what is returned from expected_success_url.

class MyViewTestCase(ViewTestCase, TestCase):
    def test_correct_redirect(self):
        response = self.request(method='POST', post_data={'name': 'Project name'})
        assert response.status_code == 302
        assert response.location == self.expected_success_url

Static success_url

Use this to define a static success URL.

Attribute: success_url

class MyViewTestCase(ViewTestCase, TestCase):
    success_url = '/path/to/project-name/'

URL name and URL arguments

To generate the expected_success_url, django-skivvy uses Django's reverse function. You have to provide the URL name and URL arguments to generate the success URL.

The URL name can be defined via the success_url_name attribute. The URL arguments can be provided by the static success_url_kwargs attribute or by implementing the method setup_success_url_kwargs if you need to apply more logic.

Attribute success_url_kwargs

class MyViewTestCase(ViewTestCase, TestCase):
    success_url_name = 'project-detail'
    success_url_kwargs = {
        'project_id': 'abc123'
    }

Method setup_success_url_kwargs

class MyViewTestCase(ViewTestCase, TestCase):
    success_url_name = 'project-detail'

    def setup_success_url_kwargs(self):
        return {
            'project_id': self.project.id
        }

Overwriting get_success_url()

Finally, overwriting the method get_success_url() provides the most flexibility.

Method get_success_url()

class MyViewTestCase(ViewTestCase, TestCase):
    def get_success_url(self):
        return '/path/to/project-name/'
django-skivvy Documentation