How to write a unit test for a django view?

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.


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.