Template testing ensures your Django templates render correctly, display the right content, and handle various data scenarios properly. While Django templates are primarily presentation logic, testing them is crucial for ensuring your application's user interface works as expected.
from django.test import TestCase, RequestFactory
from django.template import Context, Template
from django.template.loader import render_to_string
from django.contrib.auth.models import User
from blog.models import BlogPost, Category
class TemplateRenderingTests(TestCase):
"""Test basic template rendering"""
def setUp(self):
"""Set up test data"""
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.category = Category.objects.create(
name='Technology',
slug='technology'
)
self.post = BlogPost.objects.create(
title='Test Blog Post',
slug='test-blog-post',
content='This is test content for the blog post.',
author=self.user,
category=self.category,
status='published'
)
def test_template_renders_without_error(self):
"""Test template renders without syntax errors"""
template_string = """
<h1>{{ post.title }}</h1>
<p>By {{ post.author.username }}</p>
<div>{{ post.content }}</div>
"""
template = Template(template_string)
context = Context({'post': self.post})
# Should not raise TemplateSyntaxError
try:
rendered = template.render(context)
self.assertIn(self.post.title, rendered)
self.assertIn(self.user.username, rendered)
self.assertIn(self.post.content, rendered)
except Exception as e:
self.fail(f"Template rendering failed: {e}")
def test_template_with_missing_context_variable(self):
"""Test template behavior with missing context variables"""
template_string = """
<h1>{{ post.title }}</h1>
<p>{{ missing_variable }}</p>
"""
template = Template(template_string)
context = Context({'post': self.post})
rendered = template.render(context)
# Should render post title but handle missing variable gracefully
self.assertIn(self.post.title, rendered)
# Missing variables render as empty string by default
self.assertNotIn('missing_variable', rendered)
def test_template_file_rendering(self):
"""Test rendering actual template files"""
# Test using render_to_string
context = {
'post': self.post,
'user': self.user
}
try:
rendered = render_to_string('blog/post_detail.html', context)
# Check expected content is present
self.assertIn(self.post.title, rendered)
self.assertIn(self.post.content, rendered)
except Exception as e:
# Template file might not exist in test environment
self.skipTest(f"Template file not found: {e}")
def test_template_with_conditional_logic(self):
"""Test template conditional rendering"""
template_string = """
{% if post.is_published %}
<span class="published">Published</span>
{% else %}
<span class="draft">Draft</span>
{% endif %}
{% if post.author == user %}
<a href="/edit/">Edit Post</a>
{% endif %}
"""
template = Template(template_string)
# Test as post author
context = Context({
'post': self.post,
'user': self.user
})
rendered = template.render(context)
# Should show published status and edit link
self.assertIn('Published', rendered)
self.assertIn('Edit Post', rendered)
# Test as different user
other_user = User.objects.create_user('otheruser', 'other@example.com', 'pass')
context = Context({
'post': self.post,
'user': other_user
})
rendered = template.render(context)
# Should show published status but no edit link
self.assertIn('Published', rendered)
self.assertNotIn('Edit Post', rendered)
class TemplateContextTests(TestCase):
"""Test template context variables"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
# Create multiple posts for testing
for i in range(5):
BlogPost.objects.create(
title=f'Post {i}',
content=f'Content for post {i}',
author=self.user,
category=self.category,
status='published'
)
def test_view_provides_correct_context(self):
"""Test view provides correct context to template"""
response = self.client.get('/blog/')
# Check context variables
self.assertIn('posts', response.context)
self.assertIn('categories', response.context)
# Check context data
posts = response.context['posts']
self.assertEqual(len(posts), 5)
# Check all posts are published
for post in posts:
self.assertEqual(post.status, 'published')
def test_template_receives_user_context(self):
"""Test template receives user context"""
self.client.login(username='testuser', password='testpass123')
response = self.client.get('/blog/')
# Check user context
self.assertIn('user', response.context)
self.assertEqual(response.context['user'], self.user)
self.assertTrue(response.context['user'].is_authenticated)
def test_template_context_processors(self):
"""Test custom context processors"""
response = self.client.get('/blog/')
# Check context processors add expected variables
# (These would be defined in your context processors)
self.assertIn('site_name', response.context)
self.assertIn('current_year', response.context)
# Check values
self.assertEqual(response.context['site_name'], 'My Blog')
self.assertIsInstance(response.context['current_year'], int)
class BuiltinTemplateTagTests(TestCase):
"""Test built-in Django template tags"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
self.posts = []
for i in range(3):
post = BlogPost.objects.create(
title=f'Post {i}',
content=f'Content for post {i}',
author=self.user,
category=self.category,
status='published'
)
self.posts.append(post)
def test_for_loop_template_tag(self):
"""Test for loop template tag"""
template_string = """
{% for post in posts %}
<article>{{ post.title }}</article>
{% empty %}
<p>No posts available</p>
{% endfor %}
"""
template = Template(template_string)
# Test with posts
context = Context({'posts': self.posts})
rendered = template.render(context)
for post in self.posts:
self.assertIn(post.title, rendered)
self.assertNotIn('No posts available', rendered)
# Test with empty list
context = Context({'posts': []})
rendered = template.render(context)
self.assertIn('No posts available', rendered)
for post in self.posts:
self.assertNotIn(post.title, rendered)
def test_if_template_tag(self):
"""Test if template tag with various conditions"""
template_string = """
{% if posts %}
<p>{{ posts|length }} posts found</p>
{% endif %}
{% if posts|length > 2 %}
<p>Many posts</p>
{% elif posts|length > 0 %}
<p>Few posts</p>
{% else %}
<p>No posts</p>
{% endif %}
"""
template = Template(template_string)
# Test with 3 posts
context = Context({'posts': self.posts})
rendered = template.render(context)
self.assertIn('3 posts found', rendered)
self.assertIn('Many posts', rendered)
# Test with 1 post
context = Context({'posts': self.posts[:1]})
rendered = template.render(context)
self.assertIn('1 posts found', rendered)
self.assertIn('Few posts', rendered)
# Test with no posts
context = Context({'posts': []})
rendered = template.render(context)
self.assertIn('No posts', rendered)
self.assertNotIn('posts found', rendered)
def test_url_template_tag(self):
"""Test url template tag"""
template_string = """
<a href="{% url 'blog:post_detail' slug=post.slug %}">{{ post.title }}</a>
<a href="{% url 'blog:post_list' %}">All Posts</a>
"""
template = Template(template_string)
context = Context({'post': self.posts[0]})
rendered = template.render(context)
# Check URLs are generated correctly
self.assertIn(f'/blog/{self.posts[0].slug}/', rendered)
self.assertIn('/blog/', rendered)
self.assertIn(self.posts[0].title, rendered)
def test_csrf_token_template_tag(self):
"""Test CSRF token template tag"""
template_string = """
<form method="post">
{% csrf_token %}
<input type="text" name="title">
</form>
"""
# Create request with CSRF token
request = RequestFactory().get('/')
template = Template(template_string)
context = Context({'request': request})
rendered = template.render(context)
# Check CSRF token is included
self.assertIn('csrfmiddlewaretoken', rendered)
self.assertIn('type="hidden"', rendered)
# blog/templatetags/blog_tags.py
from django import template
from django.utils.safestring import mark_safe
from blog.models import BlogPost
register = template.Library()
@register.simple_tag
def recent_posts(count=5):
"""Return recent published posts"""
return BlogPost.objects.filter(status='published').order_by('-created_at')[:count]
@register.inclusion_tag('blog/tags/post_list.html')
def show_recent_posts(count=5):
"""Render recent posts using template"""
posts = BlogPost.objects.filter(status='published').order_by('-created_at')[:count]
return {'posts': posts}
@register.filter
def truncate_words_html(value, arg):
"""Truncate HTML content to specified word count"""
from django.utils.html import strip_tags
words = strip_tags(value).split()
if len(words) <= arg:
return value
truncated = ' '.join(words[:arg])
return mark_safe(f'{truncated}...')
@register.filter
def reading_time(content):
"""Calculate reading time for content"""
words = len(content.split())
minutes = max(1, words // 200) # 200 words per minute
return f"{minutes} min read"
# tests.py
from django.template import Template, Context
from django.template.loader import get_template
class CustomTemplateTagTests(TestCase):
"""Test custom template tags"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
# Create posts with different dates
from django.utils import timezone
from datetime import timedelta
for i in range(5):
post = BlogPost.objects.create(
title=f'Post {i}',
content=f'Content for post {i}',
author=self.user,
category=self.category,
status='published'
)
# Set different creation dates
post.created_at = timezone.now() - timedelta(days=i)
post.save()
def test_recent_posts_simple_tag(self):
"""Test recent_posts simple tag"""
template_string = """
{% load blog_tags %}
{% recent_posts 3 as posts %}
{% for post in posts %}
{{ post.title }}
{% endfor %}
"""
template = Template(template_string)
rendered = template.render(Context())
# Should show 3 most recent posts
self.assertIn('Post 0', rendered) # Most recent
self.assertIn('Post 1', rendered)
self.assertIn('Post 2', rendered)
self.assertNotIn('Post 3', rendered) # Older post
self.assertNotIn('Post 4', rendered) # Oldest post
def test_recent_posts_default_count(self):
"""Test recent_posts tag with default count"""
template_string = """
{% load blog_tags %}
{% recent_posts as posts %}
{{ posts|length }}
"""
template = Template(template_string)
rendered = template.render(Context())
# Should return 5 posts (all posts, default count is 5)
self.assertIn('5', rendered)
def test_show_recent_posts_inclusion_tag(self):
"""Test show_recent_posts inclusion tag"""
# Create the inclusion template
inclusion_template_content = """
<div class="recent-posts">
{% for post in posts %}
<div class="post">{{ post.title }}</div>
{% endfor %}
</div>
"""
# This would normally be in templates/blog/tags/post_list.html
# For testing, we'll mock the template loading
template_string = """
{% load blog_tags %}
{% show_recent_posts 2 %}
"""
# Note: This test would require the actual template file to exist
# In practice, you might mock the template loading or create test templates
def test_truncate_words_html_filter(self):
"""Test truncate_words_html custom filter"""
template_string = """
{% load blog_tags %}
{{ content|truncate_words_html:5 }}
"""
long_content = "This is a very long piece of content that should be truncated after five words and then some more."
template = Template(template_string)
context = Context({'content': long_content})
rendered = template.render(context)
# Should truncate after 5 words
self.assertIn('This is a very long...', rendered)
self.assertNotIn('piece of content', rendered)
def test_truncate_words_html_filter_short_content(self):
"""Test truncate_words_html filter with short content"""
template_string = """
{% load blog_tags %}
{{ content|truncate_words_html:10 }}
"""
short_content = "Short content here."
template = Template(template_string)
context = Context({'content': short_content})
rendered = template.render(context)
# Should not truncate short content
self.assertEqual(rendered.strip(), short_content)
self.assertNotIn('...', rendered)
def test_reading_time_filter(self):
"""Test reading_time custom filter"""
template_string = """
{% load blog_tags %}
{{ content|reading_time }}
"""
template = Template(template_string)
# Test short content (< 200 words)
short_content = "Short content here."
context = Context({'content': short_content})
rendered = template.render(context)
self.assertIn('1 min read', rendered)
# Test long content (400 words)
long_content = ' '.join(['word'] * 400)
context = Context({'content': long_content})
rendered = template.render(context)
self.assertIn('2 min read', rendered)
def test_custom_tag_with_invalid_arguments(self):
"""Test custom tag behavior with invalid arguments"""
template_string = """
{% load blog_tags %}
{% recent_posts "invalid" as posts %}
{{ posts|length }}
"""
template = Template(template_string)
# Should handle invalid argument gracefully
try:
rendered = template.render(Context())
# Depending on implementation, might return default or empty
except Exception as e:
# Or might raise an exception
self.assertIsInstance(e, (ValueError, TypeError))
class BuiltinFilterTests(TestCase):
"""Test built-in Django template filters"""
def test_date_filter(self):
"""Test date filter formatting"""
from django.utils import timezone
template_string = """
{{ date_value|date:"Y-m-d" }}
{{ date_value|date:"F j, Y" }}
"""
test_date = timezone.datetime(2023, 12, 25, 10, 30, 0, tzinfo=timezone.utc)
template = Template(template_string)
context = Context({'date_value': test_date})
rendered = template.render(context)
self.assertIn('2023-12-25', rendered)
self.assertIn('December 25, 2023', rendered)
def test_length_filter(self):
"""Test length filter"""
template_string = """
{{ items|length }}
{{ text|length }}
"""
template = Template(template_string)
context = Context({
'items': [1, 2, 3, 4, 5],
'text': 'Hello World'
})
rendered = template.render(context)
self.assertIn('5', rendered) # List length
self.assertIn('11', rendered) # String length
def test_default_filter(self):
"""Test default filter"""
template_string = """
{{ value|default:"No value" }}
{{ empty_value|default:"Empty" }}
"""
template = Template(template_string)
# Test with value
context = Context({'value': 'Has value', 'empty_value': ''})
rendered = template.render(context)
self.assertIn('Has value', rendered)
self.assertIn('Empty', rendered)
# Test with None
context = Context({'value': None, 'empty_value': None})
rendered = template.render(context)
self.assertIn('No value', rendered)
self.assertIn('Empty', rendered)
def test_safe_filter(self):
"""Test safe filter for HTML content"""
template_string = """
{{ html_content|safe }}
{{ html_content }}
"""
html_content = '<strong>Bold text</strong>'
template = Template(template_string)
context = Context({'html_content': html_content})
rendered = template.render(context)
# Safe version should render HTML
self.assertIn('<strong>Bold text</strong>', rendered)
# Unsafe version should escape HTML
self.assertIn('<strong>Bold text</strong>', rendered)
def test_slice_filter(self):
"""Test slice filter"""
template_string = """
{{ items|slice:":3" }}
{{ items|slice:"2:" }}
"""
items = ['a', 'b', 'c', 'd', 'e']
template = Template(template_string)
context = Context({'items': items})
rendered = template.render(context)
# First 3 items
self.assertIn("['a', 'b', 'c']", rendered)
# Items from index 2
self.assertIn("['c', 'd', 'e']", rendered)
# Additional custom filters for testing
@register.filter
def multiply(value, arg):
"""Multiply value by argument"""
try:
return float(value) * float(arg)
except (ValueError, TypeError):
return 0
@register.filter
def add_class(field, css_class):
"""Add CSS class to form field"""
return field.as_widget(attrs={'class': css_class})
@register.filter
def currency(value):
"""Format value as currency"""
try:
return f"${float(value):.2f}"
except (ValueError, TypeError):
return "$0.00"
class CustomFilterTests(TestCase):
"""Test custom template filters"""
def test_multiply_filter(self):
"""Test multiply custom filter"""
template_string = """
{% load blog_tags %}
{{ value|multiply:3 }}
{{ price|multiply:1.08 }}
"""
template = Template(template_string)
context = Context({'value': 10, 'price': 100})
rendered = template.render(context)
self.assertIn('30.0', rendered) # 10 * 3
self.assertIn('108.0', rendered) # 100 * 1.08
def test_multiply_filter_invalid_input(self):
"""Test multiply filter with invalid input"""
template_string = """
{% load blog_tags %}
{{ invalid|multiply:3 }}
"""
template = Template(template_string)
context = Context({'invalid': 'not_a_number'})
rendered = template.render(context)
self.assertIn('0', rendered) # Should return 0 for invalid input
def test_currency_filter(self):
"""Test currency custom filter"""
template_string = """
{% load blog_tags %}
{{ price1|currency }}
{{ price2|currency }}
{{ invalid|currency }}
"""
template = Template(template_string)
context = Context({
'price1': 19.99,
'price2': 100,
'invalid': 'not_a_price'
})
rendered = template.render(context)
self.assertIn('$19.99', rendered)
self.assertIn('$100.00', rendered)
self.assertIn('$0.00', rendered) # Invalid input
def test_add_class_filter_with_form_field(self):
"""Test add_class filter with form field"""
from django import forms
class TestForm(forms.Form):
name = forms.CharField(max_length=100)
template_string = """
{% load blog_tags %}
{{ form.name|add_class:"form-control" }}
"""
form = TestForm()
template = Template(template_string)
context = Context({'form': form})
rendered = template.render(context)
self.assertIn('class="form-control"', rendered)
self.assertIn('name="name"', rendered)
class TemplateInheritanceTests(TestCase):
"""Test template inheritance"""
def test_template_extends_base(self):
"""Test template extends base template"""
# Base template content
base_template = """
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>
<header>{% block header %}Default Header{% endblock %}</header>
<main>{% block content %}{% endblock %}</main>
<footer>{% block footer %}Default Footer{% endblock %}</footer>
</body>
</html>
"""
# Child template content
child_template = """
{% extends "base.html" %}
{% block title %}Custom Page Title{% endblock %}
{% block content %}
<h1>Page Content</h1>
<p>This is the main content.</p>
{% endblock %}
"""
# In a real test, you'd have these as actual template files
# For this example, we'll test the concept
# Test that child template overrides blocks correctly
template = Template(child_template)
# This would require proper template loading setup
# The test would verify that:
# - Title block is overridden
# - Content block is filled
# - Header and footer use defaults
def test_template_block_super(self):
"""Test template block.super functionality"""
# Base template with block
base_content = """
{% block content %}
<div class="base-content">Base content</div>
{% endblock %}
"""
# Child template extending base content
child_content = """
{% extends "base.html" %}
{% block content %}
{{ block.super }}
<div class="child-content">Additional content</div>
{% endblock %}
"""
# Test would verify both base and child content appear
# This requires proper template file setup
def test_template_include(self):
"""Test template include functionality"""
# Main template
main_template = """
<div class="page">
{% include "partials/header.html" %}
<main>{{ content }}</main>
{% include "partials/footer.html" %}
</div>
"""
# Test that included templates are rendered
# This requires actual template files
template = Template(main_template)
context = Context({'content': 'Main page content'})
# Would test that included content appears in output
import time
from django.test import TestCase
from django.template import Template, Context
class TemplatePerformanceTests(TestCase):
"""Test template rendering performance"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
# Create many posts for performance testing
self.posts = []
for i in range(100):
post = BlogPost.objects.create(
title=f'Post {i}',
content=f'Content for post {i}' * 50, # Longer content
author=self.user,
category=self.category,
status='published'
)
self.posts.append(post)
def test_template_rendering_performance(self):
"""Test template rendering performance with many objects"""
template_string = """
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>By {{ post.author.username }} in {{ post.category.name }}</p>
<div>{{ post.content|truncatewords:50 }}</div>
<time>{{ post.created_at|date:"F j, Y" }}</time>
</article>
{% endfor %}
"""
template = Template(template_string)
context = Context({'posts': self.posts})
# Measure rendering time
start_time = time.time()
rendered = template.render(context)
end_time = time.time()
render_time = end_time - start_time
# Assert reasonable performance (adjust threshold as needed)
self.assertLess(render_time, 1.0, "Template rendering took too long")
# Verify content was rendered
self.assertIn('Post 0', rendered)
self.assertIn('Post 99', rendered)
def test_template_caching_effectiveness(self):
"""Test template caching improves performance"""
template_string = """
{% load cache %}
{% cache 300 post_list %}
{% for post in posts %}
<div>{{ post.title }} - {{ post.created_at }}</div>
{% endfor %}
{% endcache %}
"""
template = Template(template_string)
context = Context({'posts': self.posts[:10]}) # Smaller set for caching test
# First render (should cache)
start_time = time.time()
first_render = template.render(context)
first_time = time.time() - start_time
# Second render (should use cache)
start_time = time.time()
second_render = template.render(context)
second_time = time.time() - start_time
# Cached render should be faster
self.assertLess(second_time, first_time)
# Content should be identical
self.assertEqual(first_render, second_render)
class TemplateSecurityTests(TestCase):
"""Test template security features"""
def test_auto_escaping_enabled(self):
"""Test that auto-escaping is enabled by default"""
template_string = """
{{ user_input }}
"""
malicious_input = '<script>alert("XSS")</script>'
template = Template(template_string)
context = Context({'user_input': malicious_input})
rendered = template.render(context)
# Should escape HTML
self.assertIn('<script>', rendered)
self.assertNotIn('<script>', rendered)
def test_safe_filter_bypasses_escaping(self):
"""Test that safe filter bypasses auto-escaping"""
template_string = """
{{ trusted_html|safe }}
"""
trusted_html = '<strong>Bold text</strong>'
template = Template(template_string)
context = Context({'trusted_html': trusted_html})
rendered = template.render(context)
# Should not escape trusted HTML
self.assertIn('<strong>Bold text</strong>', rendered)
self.assertNotIn('<strong>', rendered)
def test_autoescape_tag_control(self):
"""Test autoescape tag for controlling escaping"""
template_string = """
{% autoescape off %}
{{ user_input }}
{% endautoescape %}
{% autoescape on %}
{{ user_input }}
{% endautoescape %}
"""
html_input = '<em>Emphasized text</em>'
template = Template(template_string)
context = Context({'user_input': html_input})
rendered = template.render(context)
# First part should not escape (autoescape off)
self.assertIn('<em>Emphasized text</em>', rendered)
# Second part should escape (autoescape on)
self.assertIn('<em>Emphasized text</em>', rendered)
def test_template_prevents_code_injection(self):
"""Test template prevents code injection"""
# Malicious template code
template_string = """
{{ user_input }}
"""
# Attempt to inject template code
malicious_input = '{% load os %}{% os.system("rm -rf /") %}'
template = Template(template_string)
context = Context({'user_input': malicious_input})
rendered = template.render(context)
# Should render as text, not execute as template code
self.assertIn('{% load os %}', rendered)
# Should not actually execute the command
With comprehensive template testing in place, you're ready to move on to testing authentication. The next chapter will cover testing Django's authentication system, including user registration, login/logout, permissions, and custom authentication backends.
Key template testing concepts covered:
Template tests ensure your application's presentation layer works correctly, displays the right content, and maintains security standards while providing a good user experience.
Testing Forms
Form testing is essential for ensuring data validation, user input handling, and form rendering work correctly in your Django application. Forms are the primary interface for user data input, making comprehensive form testing crucial for data integrity and user experience.
Testing Authentication
Authentication testing is crucial for ensuring your Django application properly handles user registration, login, logout, permissions, and access control. Since authentication affects security and user experience, comprehensive testing helps prevent vulnerabilities and ensures reliable user management.