Django: How to easily unit test a Class Based View
Class Based Views are an integral part of my day-to-day Django applications, and thus having an adequate testing framework for them is critical. Though it is not immediately obvious how to do so, Django provides a neat solution for testing CBVs.
In this article, I will share how to leverage Django’s unit testing framework to quickly and easily get unit test coverage for class based views.
Sample model and view
The remainder of this article I will utilize the following simple model and corresponding list view below. First, our Student
model is as follows:
from django.db import models
class Student(models.Model):
name = models.CharField(max_length=128)
age = models.IntegerField()
active = models.BooleanField(default=True)
We’ll also create a very simple ListView
whose purpose is to only display Students
that are active
, below:
from django.views.generic import ListView
from .models import Student
class StudentListView(ListView):
model = Student
queryset = Student.objects.filter(active=True)
Finally, we’ll add our new view to the urls and assign it the name student-list
, below:
from django.urls import path, re_path
from .views import StudentListView
urlpatterns = [
......
re_path(r'^students/$', StudentListView.as_view(), name='student-list'),
]
So far so good. We’ve got a working page that lists all active Student
records. Next up — how can we unit test this view?
Setting up the request object
In order to test our new Class Based View, we’re going to need to first create a Request
object that can exercise the View just as if there was a browser navigating to the page. Django provides a RequestFactory
library which can instantiate one for us — all we need to do is provide the endpoint, which we can do by reversing the page name from our urls.py as shown below:
from django.test import RequestFactory
from django.urls import reverse
request = RequestFactory().get(reverse('my-view-name'))
view = StudentListView()
view.setup(request)
The factory returns a similar Request
object which functions exactly like the ones included with the TestCase
class. This object can be passed into the view to provide the necessary context needed to execute it.
In some cases, instantiating the middleware may also be necessary. Most apps will not need this, but these additional steps were necessary for my app. I added them just before running the view.setup(request)
line above.
from django.contrib.sessions.middleware import SessionMiddleware
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
Basic Unit Test
With the Request
object instantiated, we are now ready to start testing our view. Starting with a very simple “does this page load?” test, we can invoke the view by passing in the request, then we assert the HTTP status code like we would any regular HTTP unit test:
from django.test import TestCase, RequestFactory
from django.urls import reverse
from .views import StudentListView
from .models import Student
class TestStudentListViews(TestCase):
def setUp(self):
self.active_student = Student.objects.create(active=True, name='Elmo', age=3)
self.former_student = Student.objects.create(active=False, name='Snuffie', age=3)
# Set up the request and call into the view:
self.request = RequestFactory().get(reverse('student-list'))
self.view = StudentListView()
self.view.setup(self.request)
def test_view_page_loads(self):
"""
Verify the page loads for this view
"""
response = StudentListView.as_view()(self.request)
self.assertEqual(response.status_code, 200)
In the setUp
method, we create a few sample Student
records, which we will use later for further testing. Then, we instantiate and prepare the Request
object against the StudentListView
CBV.
The test_view_page_loads
method is the test itself, where we actually call into the view using the prepared Request
object, and validate it returns an HTTP 200.
We’ve written our first CBV unit test! Checking that the page loads is great, but we want to really test this CBV further than that.
Testing specific view logic
With the view now worked into our unit test framework, we can really get into the meat of it, and unit test each of the View’s methods, one at a time. For example, since this is a List
view, we can unit test the get_queryset()
method to ensure only the active records are returned. Such a test case can be seen below:
[.....]
def test_queryset(self):
"""
Verify that only active students are included in the view
"""
# Run the view's queryset and verify only active students were returned:
queryset = self.view.get_queryset()
self.assertIn(self.active_student, queryset)
self.assertNotIn(self.former_student, queryset)
In the example above, we use the assertIn
and assertNotIn
to validate that the active student is returned in this CBV’s queryset, and the inactive student is not.
Other methods in our View, whether they be custom helper methods, or inherited view methods, such as get_object()
or get_form()
as examples, can be unit tested in the same manner as the example above, and asserting the results match our expectations.
Authentication
What if some of our views requires authentication? For example, if we slightly modified our CBV to inherit from the LoginRequiredMixin
to enforce authentication on this view, how can we run the same unit tests without them breaking?
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from .models import Student
class StudentListView(LoginRequiredMixin, ListView):
model = Student
queryset = Student.objects.filter(active=True)
With the change above, our CBV now enforced users to authenticate to browse this view. Let’s make the necessary changes to our unit tests.
In order for our unit tests to work with authentication, we’ll need to create a User
object like any other model, then assign it in to the request object. This will tell the Request
object that is the authenticated user we are running the unit tests as:
from django.contrib.auth.models import User
test_user = User.objects.create(username="Test", email="test@test.test", is_active=True)
request.user = test_user
Or, if there is a need to test behaviour for unauthenticated users, i.e. to validate the view does not allow access to un-authenticated users, use the AnonymousUser
object:
from django.contrib.auth.models import AnonymousUser
request.user = AnonymousUser()
Here is an example of a few unit tests which try with both an authenticated user and an unauthenticated user:
from django.test import TestCase, RequestFactory
from django.urls import reverse
from .views import StudentListView
from .models import Student
from django.contrib.auth.models import AnonymousUser, User
class TestStudentListViews(TestCase):
def setUp(self):
self.active_student = Student.objects.create(active=True, name='Elmo', age=3)
self.former_student = Student.objects.create(active=False, name='Snuffie', age=3)
# Set up the request and call into the view:
self.request = RequestFactory().get(reverse('student-list'))
self.view = StudentListView()
self.view.setup(self.request)
def test_view_guest(self):
"""
Verify the view redirects an anonymous user to the login page.
"""
self.request.user = AnonymousUser()
self.view.setup(self.request)
response = StudentListView.as_view()(self.request)
# Verify user is redirected to the login page:
self.assertEqual(response.status_code, 302)
self.assertIn('login', response.headers.get('Location'))
def test_view_authenticated(self):
"""
Verify the view returns an HTTP 200 if the user is logged in.
"""
self.request.user = User.objects.create(username='test', is_active=True, email='a@b.com')
self.view.setup(self.request)
response = StudentListView.as_view()(self.request)
self.assertEqual(response.status_code, 200)
That’s it for this article! Django has once again provided us with the tools necessary to very easily and quickly set us up for success. Thanks to the RequestFactory
library, we can create, modify, and use the request object to instantiate and exercise our view in a black-box unit testing environment, and very quickly and simply invoke each CBV method individually. Happy testing!