Django Template Testing
When building a Django application, templates are a critical component of your user interface. Ensuring they render correctly with the expected content is an important part of a comprehensive testing strategy. In this guide, we'll explore how to effectively test Django templates to verify they function as expected.
Introduction to Template Testing
Template testing in Django focuses on verifying that:
- Templates render without errors
- The correct template is used for a particular view
- The template contains expected content
- Context variables are properly displayed
Django's testing framework provides tools to make template testing straightforward. Let's explore how to use them effectively.
Basic Template Testing Setup
Before we dive into testing templates, let's set up a basic Django project structure for our examples.
# myapp/models.py
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField()
def __str__(self):
return self.name
# myapp/views.py
from django.shortcuts import render
from .models import Product
def product_list(request):
products = Product.objects.all()
return render(request, 'myapp/product_list.html', {'products': products})
def product_detail(request, product_id):
product = Product.objects.get(id=product_id)
return render(request, 'myapp/product_detail.html', {'product': product})
And here are our templates:
<!-- myapp/templates/myapp/product_list.html -->
<!DOCTYPE html>
<html>
<head>
<title>Product List</title>
</head>
<body>
<h1>Our Products</h1>
<ul class="product-list">
{% for product in products %}
<li class="product-item">
<a href="{% url 'product_detail' product.id %}">{{ product.name }}</a> - ${{ product.price }}
</li>
{% empty %}
<li>No products available</li>
{% endfor %}
</ul>
</body>
</html>
<!-- myapp/templates/myapp/product_detail.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ product.name }}</title>
</head>
<body>
<h1>{{ product.name }}</h1>
<p class="price">Price: ${{ product.price }}</p>
<div class="description">{{ product.description }}</div>
<a href="{% url 'product_list' %}">Back to list</a>
</body>
</html>
Testing Template Rendering
Let's start with basic tests to ensure our templates render correctly.
Using Django's TestCase
# myapp/tests.py
from django.test import TestCase
from django.urls import reverse
from .models import Product
class ProductTemplateTests(TestCase):
def setUp(self):
# Create test products
self.product = Product.objects.create(
name='Test Product',
price=99.99,
description='This is a test product'
)
def test_product_list_template(self):
# Get the URL for the product list view
url = reverse('product_list')
# Make a GET request to the URL
response = self.client.get(url)
# Check that the response status code is 200 (OK)
self.assertEqual(response.status_code, 200)
# Check that the correct template was used
self.assertTemplateUsed(response, 'myapp/product_list.html')
# Check that the product appears in the template
self.assertContains(response, 'Test Product')
self.assertContains(response, '$99.99')
In this test, we:
- Create a test product in the database
- Make a request to the product list view
- Check that the response is successful
- Verify the correct template was used
- Confirm the product information appears in the rendered HTML
Testing Template Context
Let's expand our tests to verify the context variables are correctly passed to the template:
def test_product_detail_context(self):
url = reverse('product_detail', args=[self.product.id])
response = self.client.get(url)
# Check the status code
self.assertEqual(response.status_code, 200)
# Check the template used
self.assertTemplateUsed(response, 'myapp/product_detail.html')
# Check that the product is in the context
self.assertIn('product', response.context)
# Verify the product in the context is the one we created
context_product = response.context['product']
self.assertEqual(context_product.id, self.product.id)
self.assertEqual(context_product.name, 'Test Product')
self.assertEqual(float(context_product.price), 99.99)
This test focuses on the context data passed to the template. We check:
- The response context contains a 'product' key
- The product in the context matches the one we created in
setUp
Testing Template Elements with CSS Selectors
For more specific template testing, we can use the assertHTMLEqual
method or parse the HTML with a library like BeautifulSoup:
from bs4 import BeautifulSoup
def test_product_detail_elements(self):
url = reverse('product_detail', args=[self.product.id])
response = self.client.get(url)
# Parse the HTML using BeautifulSoup
soup = BeautifulSoup(response.content, 'html.parser')
# Check that the product name is in an h1 tag
h1 = soup.find('h1')
self.assertEqual(h1.text, 'Test Product')
# Check that the price is in a paragraph with class 'price'
price_p = soup.find('p', class_='price')
self.assertIsNotNone(price_p)
self.assertIn('$99.99', price_p.text)
# Check that the description is in a div with class 'description'
desc_div = soup.find('div', class_='description')
self.assertIsNotNone(desc_div)
self.assertEqual(desc_div.text, 'This is a test product')
# Check that the back link exists and points to the right URL
back_link = soup.find('a', text='Back to list')
self.assertIsNotNone(back_link)
self.assertEqual(back_link['href'], reverse('product_list'))
Using BeautifulSoup allows us to verify specific HTML elements, their content, and their attributes beyond what Django's testing tools provide natively.
Testing Template Logic
Django templates often include logic like loops, conditionals, and filters. Let's test these elements:
def test_product_list_empty(self):
# Delete all products
Product.objects.all().delete()
url = reverse('product_list')
response = self.client.get(url)
# Check the template is used
self.assertTemplateUsed(response, 'myapp/product_list.html')
# Check that the "empty" message is displayed
self.assertContains(response, 'No products available')
# Verify the product-item class doesn't appear (no products)
soup = BeautifulSoup(response.content, 'html.parser')
self.assertEqual(len(soup.find_all('li', class_='product-item')), 0)
This test verifies the template logic by:
- Removing all products from the database
- Checking that the "empty" message from our template's
{% empty %}
clause appears - Confirming no product items are rendered when the list is empty
Testing with pytest and pytest-django
If you're using pytest with the pytest-django plugin, your template tests might look slightly different:
# myapp/tests/test_templates.py
import pytest
from django.urls import reverse
from bs4 import BeautifulSoup
from myapp.models import Product
@pytest.fixture
def test_product():
return Product.objects.create(
name='Test Product',
price=99.99,
description='This is a test product'
)
@pytest.mark.django_db
def test_product_list_template(client, test_product):
url = reverse('product_list')
response = client.get(url)
assert response.status_code == 200
# Check template used (requires pytest-django)
assert 'myapp/product_list.html' in [t.name for t in response.templates]
# Check content
assert 'Test Product' in response.content.decode()
assert '$99.99' in response.content.decode()
# Check with BeautifulSoup
soup = BeautifulSoup(response.content, 'html.parser')
product_items = soup.find_all('li', class_='product-item')
assert len(product_items) == 1
assert 'Test Product' in product_items[0].text
Testing Template Inclusion and Inheritance
Django templates often use inheritance with {% extends %}
and inclusion with {% include %}
. Let's test a template that uses these features:
<!-- myapp/templates/myapp/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Site{% endblock %}</title>
</head>
<body>
<header>
<h1>My E-commerce Site</h1>
<nav>
<a href="{% url 'product_list' %}">Products</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
© 2023 My E-commerce Site
</footer>
</body>
</html>
<!-- myapp/templates/myapp/product_card.html -->
<div class="product-card">
<h3>{{ product.name }}</h3>
<p>${{ product.price }}</p>
<a href="{% url 'product_detail' product.id %}">View Details</a>
</div>
Now let's update our product list template to use these:
<!-- myapp/templates/myapp/product_list.html -->
{% extends "myapp/base.html" %}
{% block title %}Product List{% endblock %}
{% block content %}
<h2>Our Products</h2>
<div class="product-grid">
{% for product in products %}
{% include "myapp/product_card.html" with product=product %}
{% empty %}
<p>No products available</p>
{% endfor %}
</div>
{% endblock %}
And let's test this template setup:
def test_template_inheritance_and_inclusion(self):
# Create a second test product
product2 = Product.objects.create(
name='Another Product',
price=49.99,
description='This is another test product'
)
url = reverse('product_list')
response = self.client.get(url)
# Check base template elements
self.assertContains(response, '<title>Product List</title>')
self.assertContains(response, 'My E-commerce Site')
self.assertContains(response, '© 2023 My E-commerce Site')
# Check included template elements
self.assertContains(response, '<div class="product-card">')
self.assertContains(response, 'Test Product')
self.assertContains(response, 'Another Product')
# Use BeautifulSoup to check structure
soup = BeautifulSoup(response.content, 'html.parser')
# Check we have two product cards (from the included template)
product_cards = soup.find_all('div', class_='product-card')
self.assertEqual(len(product_cards), 2)
# Check the main content is within the content block
main = soup.find('main')
self.assertIsNotNone(main)
self.assertIn('Our Products', main.text)
This test verifies:
- Elements from the base template are present
- Elements from the included template appear for each product
- The correct number of product cards is rendered
- The block inheritance structure is preserved
Advanced Template Testing Techniques
Testing Custom Template Filters and Tags
If your application includes custom template filters or tags, you'll want to test them as well:
# myapp/templatetags/product_tags.py
from django import template
from django.utils.html import format_html
register = template.Library()
@register.filter
def currency(value):
return f"${value:.2f}"
@register.simple_tag
def discount_price(price, discount_percent):
discount = price * (discount_percent / 100)
final_price = price - discount
return format_html('<span class="discount">${:.2f}</span> <span class="original">${:.2f}</span>',
final_price, price)
To test these filters and tags:
def test_currency_filter(self):
from myapp.templatetags.product_tags import currency
self.assertEqual(currency(10), '$10.00')
self.assertEqual(currency(10.5), '$10.50')
self.assertEqual(currency(10.555), '$10.56') # Check rounding
def test_discount_price_tag(self):
from myapp.templatetags.product_tags import discount_price
result = discount_price(100, 20) # 20% discount on $100
self.assertIn('<span class="discount">$80.00</span>', result)
self.assertIn('<span class="original">$100.00</span>', result)
def test_custom_tags_in_template(self):
# Update our product template to use the custom filter/tag
with self.settings(TEMPLATES=[{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
'libraries': {
'product_tags': 'myapp.templatetags.product_tags',
}
},
}]):
# Load a template that uses our custom tags/filters
template_str = (
"{% load product_tags %}"
"<p>{{ product.price|currency }}</p>"
"<p>{% discount_price product.price 20 %}</p>"
)
from django.template import Template, Context
template = Template(template_str)
context = Context({'product': self.product})
rendered = template.render(context)
# Verify the output
self.assertIn('<p>$99.99</p>', rendered)
self.assertIn('<span class="discount">$79.99</span>', rendered)
self.assertIn('<span class="original">$99.99</span>', rendered)
Testing AJAX Template Responses
For templates that render AJAX responses:
def test_ajax_product_template(self):
# Update our view to handle AJAX requests
# This is typically done in the actual view, but we're mocking it here
from django.http import HttpResponse
from django.template.loader import render_to_string
def ajax_product_detail(request, product_id):
product = Product.objects.get(id=product_id)
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
html = render_to_string('myapp/product_detail_partial.html',
{'product': product}, request=request)
return HttpResponse(html)
return render(request, 'myapp/product_detail.html', {'product': product})
# Replace the view temporarily
from django.urls import path
from django.test import override_settings
urlpatterns = [
path('ajax-product/<int:product_id>/', ajax_product_detail, name='ajax_product_detail')
]
with override_settings(ROOT_URLCONF=__name__):
url = reverse('ajax_product_detail', args=[self.product.id])
# Make a normal request
response = self.client.get(url)
self.assertTemplateUsed(response, 'myapp/product_detail.html')
# Make an AJAX request
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertTemplateNotUsed(response, 'myapp/product_detail.html')
self.assertContains(response, 'Test Product')
Best Practices for Django Template Testing
-
Test Template Selection: Verify the correct template is used for each view.
-
Test Context Data: Ensure the right data is passed to the template.
-
Test Content Rendering: Check that content is correctly displayed and formatted.
-
Test Template Logic: Verify conditional logic, loops, and filters work as expected.
-
Organize Tests: Group related template tests together.
-
Use Fixtures: Create reusable test data fixtures for template testing.
-
Test Edge Cases: Empty lists, special characters, long text, etc.
-
Test for Accessibility: Verify important elements have proper accessibility attributes.
-
Test Responsiveness: If applicable, verify templates adapt to different screen sizes.
-
Test Form Rendering: Check that forms are rendered with correct fields and validation messages.
Summary
Testing Django templates is a crucial part of ensuring your application works as expected. By following the techniques in this guide, you can verify that:
- The correct templates are used
- Context data is properly passed to templates
- Content is rendered correctly
- Template logic works as expected
- Custom template tags and filters function properly
A comprehensive template testing strategy will help catch errors early and ensure a consistent user experience across your application.
Additional Resources
- Django Testing Documentation
- pytest-django Documentation
- BeautifulSoup Documentation
- Django Template Language Guide
Exercises
- Write tests for a template that displays a paginated list of products.
- Create tests for a template that includes a form with validation errors.
- Write tests for templates that use custom filters to format dates and numbers.
- Test a template that uses conditional logic to show/hide elements based on user permissions.
- Create tests for a responsive template that displays differently on mobile and desktop.
By completing these exercises, you'll gain practical experience with the various aspects of Django template testing.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)