Photo by Colin Watts on Unsplash

Django: Unit testing a CBV with forms

Adrien Van Thong
Django Unleashed
Published in
5 min readMay 15, 2024

--

In my previous article, I went over how to write unit tests which instantiate and exercise the different components of a CBV individually. Most examples were using ListView and DetailView CBVs, so in this article I’ll focus on how to unit test form-based views, for example the CreateView.

Example models

In this article, I’ll use the following model as an example:

from django.db import models

class Fruit(models.Model):
name = models.CharField(max_length=64)
price = models.DecimalField(decimal_places=2, max_digits=6)
description = models.TextField(null=True, blank=True)

This is the view I’m going to write unit tests for:

from django.views.generic import CreateView
from .models import Fruit

class FruitCreateView(CreateView):
template_name = 'form.html'
model = Fruit
fields = ['name', 'price', 'description']

def get_initial(self):
"""
Append a string on the end of the `description` field to indicate how this record was created.
User may edit this in the from.
"""
initial = super().get_initial()
initial['description'] = f"{initial.get('description', '')} Created via CreateView"
return initial

Test strategy

It’s important to note that there are two major aspects of this view that we’ll need to unit test separately:

  1. We need to test that the form is rendered and has all the elements and values we expect.
  2. We need to validate that the form, once summited, performs the desired effect (i.e. creates a new record, in the case of a CreateView)

Method 1: Using client

The first method we’ll try takes an outside-in approach: we’re just going to exercise the form URLs using the built-in Django client. For example, here is how we would write a test to verify that the page with the create form loads:

from django.test import TestCase, RequestFactory
from django.urls import reverse
from .models import Fruit

class TestFruitCreateView(TestCase):
def test_create_fruit_form_client(self):
# Verify the form loads:
r = self.client.get(reverse('fruit_create'))
self.assertEqual(r.status_code, 200)

You’ll notice that this is a pretty simple and bare-bones test. While the test checks to make sure a page is returned, we don’t yet have the ability to validate that the form has the correct fields, or that the initial values of those fields are correct. We’ll get into that aspect in the next section.

Next, we need to validate what happens when the form is submitted. In order to do that, we’ll need to construct a dictionary containing the sample values a user may try to submit to the form, then call into the CBV the same way, but this time using the POST verb instead of GET. Don’t forget to include the dictionary as the data!

    def test_create_fruit_form_submit_client(self):
# Fill out the form and submit it:
data = {
'name': 'Test Banana',
'price': 1.99,
'description': 'Test description'
}
r = self.client.post(reverse('fruit_create'), data=data)
self.assertEqual(r.status_code, 302)

# Verify the new fruit was created:
fruit = Fruit.objects.get(name=data.get('name'))
self.assertEqual(str(fruit.price), str(data.get('price')))
self.assertEqual(fruit.description, data.get('description'))

While both of these tests act as great “smoke” tests, as noted earlier, we don’t yet have the capability to validate individual components of the forms.

Method 2: Instantiating the view class

This approach is going to follow the procedure described in the previous article — we’re going to instantiate the CBV and directly call the methods in the CBV individually to ensure they return the expected values.

Starting with the first test to validate the form loading, the steps don’t differ much from what was described in the previous article, and so we end up with an extremely similar test:

    def test_create_fruit_form(self):
"""
Verify the form loads with the correct data
"""
# Verify the form loads:
request = RequestFactory().get(reverse('fruit_create'))
view = FruitCreateView()
view.setup(request)
response = FruitCreateView.as_view()(request)
self.assertEqual(response.status_code, 200)

What will be different this time is we’ll want to also exercise both the get_initial() and get_form() methods of the CBV to spot-check the fields we care about are actually included on the form and have the correct values set initially. Some sample tests to validate these methods could look like this:

        [ ... ]
# Verify the fields are on the form:
form = view.get_form()
self.assertIn('name', form.fields.keys())
self.assertIn('price', form.fields.keys())
self.assertIn('description', form.fields.keys())

# Verify the initial values on the form:
initial = view.get_initial()
self.assertIn('Created via CreateView', initial['description'])

If you have other internal custom methods in your CBV, this would be a good place to add tests for those as well.

Right away you’ll notice that although there is more to code up, we’ve gained the capability to “look into” the form object and check individual components. Without this approach, we would need to traverse the HTML document to validate the same level of information.

Next, we need to validate what happens when the form is submitted. As with the first method, we’ll need a dictionary with our form data, and pass it in the request while using the POST verb. After submitting the request, we check the HTTP response code, and we validate that a new record was created in our model:

    def test_create_fruit_form_submit(self):
"""
Verify the form can be submitted and it creates a Fruit record
"""
# Fill out the form and submit it:
data = {
'name': 'Test Banana',
'price': 1.99,
'description': 'Test description'
}
request = RequestFactory().post(reverse('fruit_create'), data=data)
view = FruitCreateView()
view.setup(request)
response = FruitCreateView.as_view()(request)
self.assertEqual(response.status_code, 302)

# Verify the new fruit was properly created:
fruit = Fruit.objects.get(name=data.get('name'))
self.assertEqual(str(fruit.price), str(data.get('price')))
self.assertEqual(fruit.description, data.get('description'))

This is also a great place to include tests which verify the validators on the form and make sure the form rejects invalid values:

    def test_create_fruit_form_submit_invalid(self):
"""
Verify the record is not created if invalid values are passed to the form:
"""
# Fill out the form and submit it:
data = {
'price': 'One ninety-nine',
'description': 'Failtest'
}
request = RequestFactory().post(reverse('fruit_create'), data=data)
view = FruitCreateView()
view.setup(request)

# Verify the form returned errors:
form = view.get_form()
self.assertFalse(form.is_valid())
self.assertTrue(form.has_error(field='price'))
self.assertTrue(form.has_error(field='name'))

# Verify the new fruit was not created:
self.assertFalse(Fruit.objects.filter(description=data.get('description')).exists())

While the form.is_valid() method simply returns a boolean letting us know whether the form data it received from the client is valid, the form.has_error() method allows us to check whether each individual field has an error. Alternatively the form.errors field contains an iterable with all the errors across all fields in a single place.

So there we have it! Two different methods to test your form-based CBVs in Django! One method requires a bit more code to get running, but grants intimate access to all the CBV’s methods, including the corresponding Form object and all of its data. The other method is much quicker and requires little setup, at the cost of access to the individual methods.

In my experience each of the two methods above fill very different and specific roles that can be useful in their own situations. The second method I’ve found very useful for more unit-level testing of CBVs with loads of custom logic and helper methods that all need to be tested individually, whereas the other is great for quickly testing very straightforward CBVs that inherit from the basic CreateView and UpdateView without much customizations.

What do you think of the approaches above? Do you have your own unique way to test CBVs? Sound off in the comments below!

--

--