How to write a unit test for a django view?

DjangoUnit Testing

Django Problem Overview


I have problems understanding how the unit tests should be designed for django.

From my understanding testing the whole view in one go seems impossible. We need to distinguish between pre-post and post states of request. But I have no idea how to design this. Is there any real life example?

Looking at the documentation the examples are too simplified and only focused on the model.

@login_required
def call_view(request, contact_id):
    profile = request.user.get_profile()
    if request.POST:        
        form = CallsForm(profile.company, request.POST)           
        if form.is_valid()
        return HttpResponseRedirect('/contact/' + contact_id + '/calls/')
    else:        
        form = CallsForm(profile.company, instance=call)              
    variables = RequestContext(request, {'form':form}
    return render_to_response('conversation.html', variables)

update:

trying to make a success test work, but it still fails:

def test_contact_view_success(self):
    # same again, but with valid data, then
    self.client.login(username='username1', password='password1')
    response = self.client.post('/contact/add/', {u'last_name': [u'Johnson'], }) 
    self.assertRedirects(response, '/')

error message:

AssertionError: Response didn't redirect as expected: Response code was 200 (expected 302)

I think this is because the form.is_valid() fails and it doesn't redirect, correct?

Django Solutions


Solution 1 - Django

NB NB! This isn't strictly a "unit test"; it's difficult and uncommon to write an independent unit test for Django view code. This is more of an integration test...

You're right that there are several pathways through your view:

  1. GET or POST by anonymous user (should redirect to login page)
  2. GET or POST by logged-in user with no profile (should raise a UserProfile.DoesNotExist exception)
  3. GET by logged-in user (should show the form)
  4. POST by logged-in user with blank data (should show form errors)
  5. POST by logged-in user with invalid data (should show form errors)
  6. POST by logged-in user with valid data (should redirect)

Testing 1 is really just testing @login_required, so you could skip it. I tend to test it anyway (just in case we've forgotten to use that decorator).

I'm not sure the failure case (a 500 error page) in 2 is what you really want. I would work out what you want to happen instead (perhaps use get_or_create(), or catch the DoesNotExist exception and create a new profile that way), or redirect to a page for the user to create a profile.

Depending on how much custom validation you have, 4 may not really need to be tested.

In any case, given all of the above, I would do something like:

from django.test import TestCase

class TestCalls(TestCase):
    def test_call_view_deny_anonymous(self):
        response = self.client.get('/url/to/view', follow=True)
        self.assertRedirects(response, '/login/')
        response = self.client.post('/url/to/view', follow=True)
        self.assertRedirects(response, '/login/')

    def test_call_view_load(self):
        self.client.login(username='user', password='test')  # defined in fixture or with factory in setUp()
        response = self.client.get('/url/to/view')
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'conversation.html')

    def test_call_view_fail_blank(self):
        self.client.login(username='user', password='test')
        response = self.client.post('/url/to/view', {}) # blank data dictionary
        self.assertFormError(response, 'form', 'some_field', 'This field is required.')
        # etc. ...

    def test_call_view_fail_invalid(self):
        # as above, but with invalid rather than blank data in dictionary

    def test_call_view_success_invalid(self):
        # same again, but with valid data, then
        self.assertRedirects(response, '/contact/1/calls/')

Obviously, a drawback here is hard-coded URLs. You could either use reverse() in your tests or build requests using RequestFactory and call your views as methods (rather than by URL). With the latter method, though, you still need to use hard-coded values or reverse() to test redirect targets.

Hope this helps.

Solution 2 - Django

Django ships with a test client which can be used to test the full request/response cycle: The docs contain an example of making a get request to a given url and asserting the status code as well as the template context. You would also need a test which does a POST and asserts a successful redirect as expected.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionHoumanView Question on Stackoverflow
Solution 1 - DjangosupervacuoView Answer on Stackoverflow
Solution 2 - DjangoMark LavinView Answer on Stackoverflow