Unit Testing Forms in Django

This is a subject that doesn’t seem to be covered well by other sources, so I am recording my notes on testing a Django form. Credit to “fRui Apps” for this post that gives an example. (See the section Testing Forms)

My goal is to get the test as close to the code as possible. Rather than using the view test, or even Selenium, this approach will isolate the form’s functions without going through all of Django’s URL routing, middleware, etc. Save that for the functional tests.

In this first example, my form will pad the entered number with left hand zeroes. This is the quick test:

def test_id_format(self):  
    ## Use factoryboy to create a model instance  
    e = EndowmentFactory.create()
    ## Instantiate form  
    form = EndowmentEntityForm(data={'endowment': e.pk,  
                                     'id_number': '528989',  
                                     'entity_type': EntityType.ORIGINATOR,})
    ## call is_valid() to create cleaned_data  
    self.assertTrue(form.is_valid())  
    ## Test away!  
    self.assertEqual(form.clean_id_number(),'0000528989')

Note that I’m using factoryboy to create a model instance to work with. The form data includes an id_number of fewer than 10 characters. After I call is_valid() to populate cleaned_data, I can test the cleaned id_number for the proper format.

In this next example, I am testing for valid and invalid data. My application allows changing the status of an object, but certain rules apply. Here is a test confirming that a valid change is allowed to continue:

##  Test for valid action types
def test_action_type_valid(self):
    ## Use factoryboy to create a model instance
    e = EndowmentFactory.create(status='E')        
    ## Instantiate form
    form = UserActionForm(data={'endowment': e.pk,
                                'username': 'dgentry',
                                'action_type': 'A',})
    ## is_valid() should return True if no errors are found
    self.assertTrue(form.is_valid())

And this will test for an invalid status change:

##  Test for valid action types
def test_action_type_valid(self):
    ## Use factoryboy to create a model instance
    e = EndowmentFactory.create(status='A')        
    ## Instantiate form
    form = UserActionForm(data={'endowment': e.pk,
                                'username': 'dgentry',
                                'action_type': 'E',})
    ## We run is_valid() and expect False
    self.failIf(form.is_valid())
    ## check for the error
    self.assertIn(u'Invalid Action',form.errors['__all__'])

I’m sure that I will learn more about testing forms as I go. Stay tuned…

Lazy Reverse

Working in Django 1.3, I had some trouble with my view code.

Actually, the error was pretty vague, something about my urls.py containing no patterns. After much wailing and gnashing of teeth (during which I learned I really don’t know how to best use the debugger in Eclipse), I traced the problem down to three of my view classes. All three included a setting for the success_url attribute:

success_url = reverse('support_list')

Apparently, you can’t do that.

Once I found the offending line of code, something in my mind clicked on to remind me that I had hit this problem before. To solve it, I had moved the reverse statement into the get_success_url() function:

def get_success_url()
    return reverse('support_list')

Looking for a better solution, a quick Google Search brought up this post in in the django-users group.

I have a CreateView which I’d like to redirect to a custom success_url

defined in my URLconf. As I want to stick to the DRY-principle I just

did the following:

  success_url = reverse("my-named-url") 

Unfortunately, this breaks my site by raising an

“ImproperlyConfigured: The included urlconf doesn’t have any patterns

in it”. Removing success_url and setting the model’s

get_absolute_url() to the following works fine:

  def get_absolute_url(self): 
      return reverse("my-named-url") 

I could reproduce this with a brand new project/application so I don’t

think this has something to do with my setup.

Can anyone confirm this issue?

So others have hit the same problem. Looking further down the page, a more concise solution was offered:

 success_url = lazy(reverse, str)("support_list") 

I used this for my current project – a little cleaner.

In v1.4, we can use the ‘reverse_lazy()’ function in place of ‘reverse()’.

UPDATE:

Suddenly, I understand the reason for the SuccessURLRedirectListMixin in the popular Django Braces package. Using this mixin, one can declare the success url with a simple variable assignment (no lazy code required), and the mixin provides the get_success_url() function needed to make everything work.

BTW, I highly recommend using Braces with class based views. I have found several to be quite useful to speed up coding and to keep things clean and clear.