Monday, March 4, 2013

Django Testing Examples

Django's testing functionality is awesome. But the docs are somewhat lacking. Here's a great post on stackoverflow that will get you started. Here are a few more examples:

Fixtures

These are databases that are used only for testing. They have known, fixed content that the tests can test against. The easiest way to make a fixture is to run:

python manage.py dumpdata

The problem with that approach is it will include stuff that will break the fixture. What you need are some excludes. And to make things more readable, you might want to add some indents. Here is the command I used:

python manage.py dumpdata --indent 4 -e contenttypes -e auth.Permission -e sessions -e admin.logentry > simple_auth_fixture.json

Note, this will not remove those tables from your testing database. It will just not provide initial data for those tables.

Also, I use south for database migrations. South makes database entries and .py files to track migrations. By not excluding south there was the potential for a conflict with my migration .py files. This did not happen in the tests I did.


Testing an Object Edit View

The usual flow is for the user to load the form using initial data, then edit the data and post it. Here is one way to do that:

response = self.client.get('/app/edit_obj/3/', follow=True)
# Make sure the form loads
self.assertTemplateUsed(response, 'app/edit_obj.html')
# Get the initial form data 
form_data = response.context['form'].initial 

# Now you can change some data and post it
form_data['afield']='xyz'
response = self.client.post('/app/edit_obj/3/', form_data, follow=True)
# Use asserts to handle whatever responses you expect.

Figuring Out Unexpected Errors

Lets say you are testing a view that includes a form. You post some data and get an unexpected error. How do you figure out what the error is? Here's one way:

response = self.client.post('/app/edit_obj/3/', form_data, follow=True)

# These print statements should help you figure out what went wrong 
print 'Errors:', response.context['form'].errors
print 'Non Field Errors:',response.context['form'].non_field_errors
print 'Is Valid',response.context['form'].is_valid()

A Confusing Error from assertFormError

Here is a somewhat confusing error I got when using assertFormError. The error was:

Failure
Traceback (most recent call last):
  File "/home/junk/.virtualenvs/qdb_django14/lib/python2.7/site-packages/my_proj/an_app/tests.py", line 52, in test_call_view_fails_dup
    self.assertFormError(response, 'form', 'email', u"Email address already exists.")
  File "/home/junk/.virtualenvs/qdb_django14/lib/python2.7/site-packages/django/test/testcases.py", line 718, in assertFormError
    " response" % form)
AssertionError: The form 'form' was not used to render the response

The problem was the data I was sending to the form was supposed to be invalid. But there was an error in my form validation code. The method form.is_valid() returned true which lead to an unexpected redirect. The new page did not use the context variable 'form'. It would have been really confusing if the new page also had a context variable named 'form'.

One way to prevent this is to use assertTemplateUsed to make sure you have the correct template before you look for errors.

Testing by User Type

Often it is important to restrict certain activities to certain types of users. Since I made my fixture from a working website, it already contains the various types of users I might want to run a test on. The problem with this is I did not want to put passwords in my tests.

One way to handle that is just to reset each user's password before login. I made a method for that:

class TestEditClient(TestCase):
    def login(self,username):
        # Keep real password out of tests. Sets password to test.
        self.password='test'
        self.user = User.objects.get(username__exact=username)
        self.user.set_password(self.password)
        self.user.save()        
        response = self.client.login(username=username, password=self.password)
        if not response:
            raise RuntimeError('Could not login') 

An alternative would have been to create each user type using the standard commands for creating a user. For my project, users had a bunch of other records associated with them. I did not feel like creating those, so I just used the users we already had in the db.

No comments:

Post a Comment