IAN WALDRON IAN WALDRON

Template Tests for Django TemplateView

A mixin to be used with testing Django TemplateView class-based views to reduce writing redundant test cases.
October 27, 2023

Background

In the previous article How to Test a Django TemplateView we looked at what basic tests are appropriate for Django's TemplateView. In this article, we'll take this process a step further and abstract the tests as a mixin. In doing so, we'll be able to considerably cut down on the amount of code we're writing to test our views.

Set Up

We concluded our test of the Django TemplateView with the following test:


from django.test import TestCase
from django.urls import reverse


class MyTest(TestCase):

    @classmethod
    def setUpTestData(cls):
        cls.reversed_url = reverse("pattern-name")
        cls.static_url = "/some-static-url/"

    def test_uses_correct_template(self):
        response = self.client.get(self.reversed_url)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, "my_app/my-template.html")

    def test_urls(self):
        # response checked in test_uses_correct_template, no need to check again here
        self.assertEqual(self.reversed_url, self.static_url)

With this test, we established a bit of set-up data with the 'setUpTestData' method, we tested both that the expected template was used and that our reversed URL provides a good status code with our 'test_uses_correct_template' test, and finally we ensured that our view exists at the URL we expect with our 'test_urls' test. We'll be doing essentially the same tests with our mixin.

The Tests

Our mixin, which we'll name 'ViewTestMixin,' will have the same two methods. But instead of establishing data with our 'setUpTestData' method, we'll add checks to each test to ensure the data established with this method, already exists. Additionally, we'll change the template name from a statically coded value to an attribute.


# noinspection PyUnresolvedReferences
class ViewTestMixin:  # pragma: no cover
    """
    For use w/o user or permissions; public pages etc.
    """

    def _check_attr(self, attr_name):
        self.assertIsNotNone(
            getattr(self, attr_name, None),
            msg=f"The required attribute '{attr_name}' has not been set causing the test to fail."
        )

    def test_uses_correct_template(self):
        # check w/o user or perms
        self._check_attr("reversed_url")
        self._check_attr("template_name")
        response = self.client.get(self.reversed_url)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, self.template_name)

    def test_view_url(self):
        self._check_attr("reversed_url")
        self._check_attr("static_url")
        # response checked in correct_template so no need to use client here
        self.assertEqual(self.reversed_url, self.static_url)

Here we have three attributes specific to this mixin: 'template_name,' 'reversed_url,' and 'static_url.' Normally, we would define these attributes in our __init__ dunder method. But with testing, doing so would likely have unintended consequences. Instead, the attributes that a given method requires are checked for at the beginning of execution. I even created a tidy method to do the testing so we're not repeating code.

For example, the 'test_view_url' method requires two attributes: 'reversed_url' and 'static_url.' The first two lines of the test pass the attribute names to the '_check_attr' class method which checks if the desired attribute is present and not None. If None, an AssertionError is raised using our predefined message. With this approach, we can be sure that our tests are being executed as we expect.

A couple things I want to note with the above approach. First, it appears that the check for 'reversed_url' may be redundant because the upstream test 'test_uses_correct_template' already checks for this value. It's important to understand how execution order affects tests. The tests will be discovered and loaded alphabetically, not chronologically. In this case, alphabetical order and chronological order are the same. We can see this by comparing the strings of the method names:


>>> "test_uses_correct_template" < "test_view_url"
True

We could play with this a bit further and name our tests with prefixing like 'test_a_test_something,' 'test_b_test_another_thing' to ensure that our tests are loaded in a particular fashion. But this would create other problems (besides reducing readability) if the ordering is manually overridden elsewhere. Additionally, if tests are run in parallel, then you won't have order preserved. For this reason, and still others not mentioned, tests structured like these need to be independent of one another.

Last, there are a couple of commented values in the test that are for the benefit of the IDE as well as coverage testing. Using IDE's like PyCharm, the IDE will try to resolve attribute and method names. Since inheritance doesn't occur until this class is 'mixed in' at the construction of the test, many of the references above won't be available and the IDE will complain. Using '# noinspection PyUnresovledReferences' will silent that complaint. Also, this mixin will be tested for coverage if we don't add '# pragma: no cover' at the class level. This will exclude our test mixin from being included in coverage testing.

Use

With our basic tests defined as a mixin, all we need to do is import our mixin, extend our test class, and define our attributes. Let's say we have the following Django TemplateView and URL:


# views.py
from django.views.generic import TemplateView


class HomeView(TemplateView):
    template_name = "app_name/home.html"


# urls.py
from django.urls import path

from app_name.views import HomeView


urlpatterns = [
    path("", HomeView.as_view(), name="home"),
]

All we need to do in constructing a comprehensive test is:


from django.test import TestCase
from django.urls import reverse

from utils.test.mixins import ViewTestMixin


class TestHomeView(ViewTestMixin, TestCase):

    @classmethod
    def setUpTestData(cls):
        cls.template_name = "app_name/home.html"
        cls.reversed_url = reverse("home")
        cls.static_url = "/"

Our tests incorporated by the mixin will retrieve the attributes set with the above 'setUpTestData' method. Rather than re-writing these basic tests for each view, add the mixin to your test class, set the attributes, and you'll be able to cut down on repetitive tests freeing up your focus on more involved code.

Final Thoughts

Writing tests can become repetitive. Reduce the amount of code you're writing for tests by abstracting the most repeated components with mixins and supply only what's changing between the tests.

Source code can be found on GitHub.