Testing views for Django involves a lot of repetitive code. Each test case evaluates similar cases:
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.
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
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
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={})
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. |
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
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.
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'
)
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.
view_class
from myapp.views import MyView
class MyViewTestCase(ViewTestCase, TestCase):
view_class = MyView
setup_view()
from myapp.views import MyView
class MyViewTestCase(ViewTestCase, TestCase):
def setup_view(self):
return MyView.as_view()
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.
url_kwargs
from myapp.views import MyView
class MyViewTestCase(ViewTestCase, TestCase):
url_kwargs = {
'project_id': 'abc123'
}
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'})
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.
get_data
from myapp.views import MyView
class MyViewTestCase(ViewTestCase, TestCase):
get_data = {
'search': 'foo'
}
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'})
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.
request_meta
from myapp.views import MyView
class MyViewTestCase(ViewTestCase, TestCase):
request_meta = {
'HTTP_REFERER': 'http://example.com'
}
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'})
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:
post_data
from myapp.views import MyView
class MyViewTestCase(ViewTestCase, TestCase):
post_data = {
'name': 'New name'
}
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'})
If the view class you are testing is a ViewSet
, then you have to configure viewset_actions
in the test case.
viewset_actions
class MyViewTestCase(APITestCase, TestCase):
view_class = ViewSetInstance
viewset_actions = {'get': 'list'}
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
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.
template
class MyViewTestCase(ViewTestCase, TestCase):
template = 'projects/project_detail.html'
setup_template
class MyViewTestCase(ViewTestCase, TestCase):
def setup_template(self):
return 'projects/project_detail.html'
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.
template_context
class MyViewTestCase(ViewTestCase, TestCase):
template_context = {
'object_id': 'abc123'
}
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
}
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
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)
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
success_url
Use this to define a static success URL.
success_url
class MyViewTestCase(ViewTestCase, TestCase):
success_url = '/path/to/project-name/'
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.
success_url_kwargs
class MyViewTestCase(ViewTestCase, TestCase):
success_url_name = 'project-detail'
success_url_kwargs = {
'project_id': 'abc123'
}
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
}
get_success_url()
Finally, overwriting the method get_success_url()
provides the most flexibility.
get_success_url()
class MyViewTestCase(ViewTestCase, TestCase):
def get_success_url(self):
return '/path/to/project-name/'