Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 55

Testing Django Views with the Test Client

Recommended resources

Hello! Welcome back.

In our last lesson, we laid the groundwork for testing by focusing on the "M" in Django's MVT architecture: the models. You learned how to use django.test.TestCase to verify your model's field attributes and custom methods, ensuring your data layer is solid.

Today, we move up the stack to test the "V": the views. Views are the bridge between your data and the user, handling incoming requests and crafting responses. Testing them is crucial to ensure your application behaves correctly from a user's perspective.

By the end of this lesson, you will be able to simulate user interactions like visiting a page, submitting a form, or deleting an item, and verify that your views respond exactly as you expect. This involves:

  • Using Django's test client to make simulated HTTP requests (GET, POST, etc.).
  • Asserting response status codes (e.g., 200 OK, 302 Redirect, 404 Not Found).
  • Verifying that the correct template is rendered.
  • Checking the data passed to the template via the context.
  • Confirming that database changes occur when they should.

Let's begin.

1. The Django Test Client: Your Automated Browser

In the previous lesson, we used django.test.TestCase, which provides an isolated database for each test run. This class also comes with a powerful tool attached to it: self.client.

You can think of self.client as a dummy web browser that you control with Python code. It allows you to "visit" URLs in your application, submit data, and inspect the response, all without needing to run a development server or open a real browser.

Django Request-Response Cycle
This diagram shows the full request-response cycle. The test client programmatically initiates this cycle, allowing us to inspect the final response without the overhead of a real HTTP server and network communication.

The basic workflow for testing a view is:

  1. Arrange: Set up any necessary database state (e.g., create a Post object to be displayed).
  2. Act: Use self.client to make a request to a specific URL.
  3. Assert: Check that the response from the view is correct.

Testing tools | Django documentation

The official Django documentation provides the definitive guide to the test client. Let's review the key sections that introduce the client and its capabilities.

Please read the sections 'The test client' and 'Default test client'. These sections explain what the client is, what it's used for, and how it's automatically available as self.client in your test cases.


2. Testing Simple GET Requests

The most common type of view test is for a simple GET request, such as loading a list or detail page. Let's imagine we have a view that lists all blog posts. Our test should verify three main things:

  1. Does the page load successfully (Status Code 200)?
  2. Does it use the correct template?
  3. Does it contain the data we expect?

Mastering Django Unit Testing: An In-Depth Introduction and Tutorial

This video provides a clear, practical demonstration of testing a simple GET request for an index page. Pay close attention to the assertions used.

Watch the segment from 18:50 to 21:32. The author tests an index view, asserting the status code is 200 and that the correct template is used via assertTemplateUsed.

Example: Testing a Post List View

Let's apply this to a PostListView. Assume we have a blog app with a URL named 'post_list'.

# blog/tests/test_views.py
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from ..models import Post

class PostViewsTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Create a user and a few posts
        user = User.objects.create_user(username='testuser', password='password')
        number_of_posts = 5
        for post_id in range(number_of_posts):
            Post.objects.create(
                title=f'Post {post_id}',
                content=f'Content for post {post_id}.',
                author=user,
            )

    def test_post_list_view_at_correct_location(self):
        # Get the response from the client
        response = self.client.get('/blog/') 
        self.assertEqual(response.status_code, 200)

    def test_post_list_view_accessible_by_name(self):
        # Using reverse() is best practice as it's not hardcoded
        response = self.client.get(reverse('blog:post_list'))
        self.assertEqual(response.status_code, 200)

    def test_post_list_view_uses_correct_template(self):
        response = self.client.get(reverse('blog:post_list'))
        self.assertEqual(response.status_code, 200)
        # Check that the expected template was used to render the response
        self.assertTemplateUsed(response, 'blog/post_list.html')

    def test_post_list_view_pagination(self):
        # Assuming paginate_by = 3 in the view
        response = self.client.get(reverse('blog:post_list'))
        self.assertEqual(response.status_code, 200)
        # The view passes a context variable 'is_paginated'
        self.assertTrue(response.context['is_paginated'])
        # Check the number of posts on the first page
        self.assertEqual(len(response.context['post_list']), 3)

    def test_post_list_view_shows_all_posts_on_second_page(self):
        # Request the second page
        response = self.client.get(reverse('blog:post_list') + '?page=2')
        self.assertEqual(response.status_code, 200)
        self.assertTrue(response.context['is_paginated'])
        # Check the number of posts on the second page (5 total - 3 on first page)
        self.assertEqual(len(response.context['post_list']), 2)

Notice how we can inspect response.context. This dictionary contains all the data passed from the view to the template, which is incredibly useful for verifying the view's logic.


3. Testing Authenticated Views

Many views should only be accessible to logged-in users. Django's test client provides helpers to manage authentication state.

You need to test two scenarios:

  1. An unauthenticated user is redirected to the login page.
  2. An authenticated user can access the page successfully.

Django Tutorial Part 10: Testing a Django web application

The MDN tutorial has an excellent section on testing views that are restricted to logged-in users. It demonstrates how to log a user in and test for redirects.

Read the section 'Views that are restricted to logged in users'. Focus on the test_redirect_if_not_logged_in and test_logged_in_uses_correct_template methods. Note the use of self.client.login() and self.assertRedirects().

Example: Testing a "My Posts" View

Let's say we have a view at /blog/my-posts/ (URL name 'my_posts') that is protected by LoginRequiredMixin.

# In your test class...

def test_my_posts_view_redirects_if_not_logged_in(self):
    response = self.client.get(reverse('blog:my_posts'))
    # Assert that the status code is 302 (redirect)
    self.assertEqual(response.status_code, 302)
    # You can also use assertRedirects for a more robust check
    self.assertRedirects(response, '/accounts/login/?next=/blog/my-posts/')

def test_my_posts_view_for_logged_in_user(self):
    # Log the user in
    self.client.login(username='testuser', password='password')
    
    # Now, make the request as a logged-in user
    response = self.client.get(reverse('blog:my_posts'))
    
    # Check that the view is now accessible
    self.assertEqual(response.status_code, 200)
    self.assertTemplateUsed(response, 'blog/my_posts.html')
    # You could also check that the context only contains posts by 'testuser'

4. Testing Views that Change Data (POST, DELETE)

Testing views that handle form submissions (POST) or deletions (DELETE) follows a similar pattern, but with an important addition: you must verify that the database state has changed correctly.

Django Testing Tutorial - Testing Views #3

This video walks through testing create, delete, and update operations, which map directly to POST and DELETE requests.

Watch the segments on testing a POST request (06:26 - 09:28) and a DELETE request (10:11 - 12:14). Observe how data is sent with the request and how the tests assert both the response and the change in the database (e.g., a new expense is added, or the count becomes zero after deletion).

Example: Testing Post Creation and Deletion

Let's assume we have a view to create a new post (POST to /blog/new/) and a view to delete one (POST or DELETE to /blog/1/delete/).

# In your test class...

def test_create_post_view(self):
    self.client.login(username='testuser', password='password')
    initial_post_count = Post.objects.count()
    
    # The data for the new post
    post_data = {
        'title': 'A New Post',
        'content': 'This is the content.'
    }
    
    # Make a POST request to the 'post_create' URL
    response = self.client.post(reverse('blog:post_create'), data=post_data)
    
    # On success, a create view usually redirects
    self.assertEqual(response.status_code, 302)
    
    # Check that a new post was created
    self.assertEqual(Post.objects.count(), initial_post_count + 1)
    
    # Check that the new post has the correct title
    last_post = Post.objects.last()
    self.assertEqual(last_post.title, 'A New Post')

def test_delete_post_view(self):
    self.client.login(username='testuser', password='password')
    post_to_delete = Post.objects.get(id=1)
    initial_post_count = Post.objects.count()
    
    # Make a POST request to the 'post_delete' URL
    response = self.client.post(reverse('blog:post_delete', args=[post_to_delete.id]))
    
    # Should redirect after deletion
    self.assertEqual(response.status_code, 302)
    
    # Check that the post was deleted
    self.assertEqual(Post.objects.count(), initial_post_count - 1)
Test your understanding!

Imagine you have a CreateView for adding a Product. A POST request with an empty name field should not create a product and should re-render the form page with an error.

How would you test this scenario? What status code would you expect, and what else would you check?

Show answer

You would expect a status code of 200, not a redirect. A failed form validation typically re-renders the same page with the form, including error messages.

Here's how you could test it:

def test_create_product_with_invalid_data(self):
    self.client.login(username='testuser', password='password')
    initial_product_count = Product.objects.count()
    
    # POST request with invalid data (empty name)
    response = self.client.post(reverse('product_create'), data={'name': '', 'price': '19.99'})
    
    # 1. Assert the page re-renders successfully (status 200)
    self.assertEqual(response.status_code, 200)
    
    # 2. Assert no new product was created in the database
    self.assertEqual(Product.objects.count(), initial_product_count)
    
    # 3. (Advanced) Assert that the form in the context contains an error for the 'name' field
    self.assertFormError(response, 'form', 'name', 'This field is required.')

The assertFormError is a specific and very useful assertion for testing form validation.


Conclusion

In this lesson, you've learned how to thoroughly test your Django views, a critical skill for building robust web applications. By simulating user requests, you can automatically verify that your application's logic is correct from end to end.

Key Takeaways:

  • self.client is your automated browser: Use it within TestCase to simulate GET, POST, and other HTTP requests.
  • A view test checks the entire response: This includes the status code, the template used, the context data, and any redirects.
  • Test for all user states: Always check how a view behaves for both authenticated and unauthenticated users if it's protected.
  • Verify database side-effects: For views that create, update, or delete data, always assert that the database was modified correctly.

Next Up

You've now tested both the "M" and "V" of Django's server-rendered architecture. As per your goal to learn backend development, our next set of lessons will pivot to building and testing REST APIs with the Django REST Framework (DRF). In the very next lesson, we will start by installing DRF and learning about its core component: Serializers.