View testing is crucial for ensuring your Django application handles HTTP requests correctly, renders appropriate responses, and enforces proper access controls. Views are the interface between your users and your application logic, making comprehensive view testing essential for a reliable web application.
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from django.http import Http404
from blog.models import BlogPost, Category
class BlogViewTests(TestCase):
"""Test blog views"""
def setUp(self):
"""Set up test data"""
self.client = Client()
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 Post',
slug='test-post',
content='This is test content.',
author=self.user,
category=self.category,
status='published'
)
def test_post_list_view_get(self):
"""Test blog post list view GET request"""
url = reverse('blog:post_list')
response = self.client.get(url)
# Check response status
self.assertEqual(response.status_code, 200)
# Check template used
self.assertTemplateUsed(response, 'blog/post_list.html')
# Check context data
self.assertIn('posts', response.context)
self.assertIn(self.post, response.context['posts'])
# Check response content
self.assertContains(response, self.post.title)
self.assertContains(response, self.post.author.username)
def test_post_detail_view_get(self):
"""Test blog post detail view GET request"""
url = reverse('blog:post_detail', kwargs={'slug': self.post.slug})
response = self.client.get(url)
# Check response status
self.assertEqual(response.status_code, 200)
# Check template used
self.assertTemplateUsed(response, 'blog/post_detail.html')
# Check context data
self.assertEqual(response.context['post'], self.post)
# Check response content
self.assertContains(response, self.post.title)
self.assertContains(response, self.post.content)
self.assertContains(response, self.post.author.username)
def test_post_detail_view_nonexistent_post(self):
"""Test post detail view with nonexistent post"""
url = reverse('blog:post_detail', kwargs={'slug': 'nonexistent-post'})
response = self.client.get(url)
# Should return 404
self.assertEqual(response.status_code, 404)
def test_post_create_view_get_authenticated(self):
"""Test post create view GET request when authenticated"""
self.client.login(username='testuser', password='testpass123')
url = reverse('blog:post_create')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'blog/post_form.html')
self.assertIn('form', response.context)
def test_post_create_view_get_anonymous(self):
"""Test post create view GET request when anonymous"""
url = reverse('blog:post_create')
response = self.client.get(url)
# Should redirect to login
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, f'/login/?next={url}')
def test_post_create_view_post_valid_data(self):
"""Test post create view POST request with valid data"""
self.client.login(username='testuser', password='testpass123')
url = reverse('blog:post_create')
data = {
'title': 'New Test Post',
'content': 'This is new test content.',
'category': self.category.id,
'status': 'published'
}
response = self.client.post(url, data)
# Should redirect after successful creation
self.assertEqual(response.status_code, 302)
# Check post was created
new_post = BlogPost.objects.get(title='New Test Post')
self.assertEqual(new_post.author, self.user)
self.assertEqual(new_post.category, self.category)
# Check redirect location
self.assertRedirects(response, new_post.get_absolute_url())
def test_post_create_view_post_invalid_data(self):
"""Test post create view POST request with invalid data"""
self.client.login(username='testuser', password='testpass123')
url = reverse('blog:post_create')
data = {
'title': '', # Invalid: empty title
'content': 'Content',
'category': self.category.id
}
response = self.client.post(url, data)
# Should return form with errors
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'blog/post_form.html')
self.assertFormError(response, 'form', 'title', 'This field is required.')
# Check no post was created
self.assertFalse(BlogPost.objects.filter(title='').exists())
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from django.core.paginator import Page
class BlogClassBasedViewTests(TestCase):
"""Test class-based views"""
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(
name='Technology',
slug='technology'
)
# Create multiple posts for pagination testing
for i in range(25):
BlogPost.objects.create(
title=f'Test Post {i}',
slug=f'test-post-{i}',
content=f'Content for post {i}',
author=self.user,
category=self.category,
status='published'
)
def test_post_list_view_pagination(self):
"""Test ListView pagination"""
url = reverse('blog:post_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# Check pagination context
self.assertIn('is_paginated', response.context)
self.assertTrue(response.context['is_paginated'])
# Check page object
page_obj = response.context['page_obj']
self.assertIsInstance(page_obj, Page)
self.assertEqual(page_obj.number, 1)
# Check posts per page (assuming 20 per page)
posts = response.context['posts']
self.assertEqual(len(posts), 20)
def test_post_list_view_second_page(self):
"""Test ListView second page"""
url = reverse('blog:post_list')
response = self.client.get(url, {'page': 2})
self.assertEqual(response.status_code, 200)
page_obj = response.context['page_obj']
self.assertEqual(page_obj.number, 2)
# Should have remaining posts
posts = response.context['posts']
self.assertEqual(len(posts), 5) # 25 total - 20 on first page
def test_post_list_view_invalid_page(self):
"""Test ListView with invalid page number"""
url = reverse('blog:post_list')
response = self.client.get(url, {'page': 999})
# Should show last page
self.assertEqual(response.status_code, 200)
page_obj = response.context['page_obj']
self.assertEqual(page_obj.number, 2) # Last page
def test_post_detail_view_context(self):
"""Test DetailView context"""
post = BlogPost.objects.first()
url = reverse('blog:post_detail', kwargs={'slug': post.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['post'], post)
self.assertEqual(response.context['object'], post) # Generic context name
def test_post_update_view_owner_access(self):
"""Test UpdateView access by post owner"""
post = BlogPost.objects.first()
self.client.login(username='testuser', password='testpass123')
url = reverse('blog:post_update', kwargs={'slug': post.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'blog/post_form.html')
# Check form is pre-populated
form = response.context['form']
self.assertEqual(form.initial['title'], post.title)
def test_post_update_view_non_owner_access(self):
"""Test UpdateView access by non-owner"""
# Create another user
other_user = User.objects.create_user(
username='otheruser',
password='otherpass123'
)
post = BlogPost.objects.first()
self.client.login(username='otheruser', password='otherpass123')
url = reverse('blog:post_update', kwargs={'slug': post.slug})
response = self.client.get(url)
# Should be forbidden or redirect
self.assertIn(response.status_code, [403, 302])
def test_post_delete_view_confirmation(self):
"""Test DeleteView confirmation page"""
post = BlogPost.objects.first()
self.client.login(username='testuser', password='testpass123')
url = reverse('blog:post_delete', kwargs={'slug': post.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'blog/post_confirm_delete.html')
self.assertEqual(response.context['post'], post)
def test_post_delete_view_post_request(self):
"""Test DeleteView POST request"""
post = BlogPost.objects.first()
post_id = post.id
self.client.login(username='testuser', password='testpass123')
url = reverse('blog:post_delete', kwargs={'slug': post.slug})
response = self.client.post(url)
# Should redirect after deletion
self.assertEqual(response.status_code, 302)
# Check post was deleted
self.assertFalse(BlogPost.objects.filter(id=post_id).exists())
# Check redirect location
self.assertRedirects(response, reverse('blog:post_list'))
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
class AuthenticationTests(TestCase):
"""Test authentication requirements"""
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(
name='Technology',
slug='technology'
)
def test_login_required_view_anonymous_user(self):
"""Test login required view with anonymous user"""
url = reverse('blog:post_create')
response = self.client.get(url)
# Should redirect to login
self.assertEqual(response.status_code, 302)
# Check redirect URL includes next parameter
expected_url = f'/accounts/login/?next={url}'
self.assertRedirects(response, expected_url)
def test_login_required_view_authenticated_user(self):
"""Test login required view with authenticated user"""
self.client.login(username='testuser', password='testpass123')
url = reverse('blog:post_create')
response = self.client.get(url)
# Should allow access
self.assertEqual(response.status_code, 200)
def test_force_login_method(self):
"""Test using force_login for authentication"""
# force_login bypasses authentication backend
self.client.force_login(self.user)
url = reverse('blog:post_create')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_logout_functionality(self):
"""Test user logout"""
# Login first
self.client.login(username='testuser', password='testpass123')
# Access protected view (should work)
url = reverse('blog:post_create')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# Logout
self.client.logout()
# Try to access protected view again (should redirect)
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
class PermissionTests(TestCase):
"""Test permission-based access control"""
def setUp(self):
# Create users
self.regular_user = User.objects.create_user(
username='regular',
password='pass123'
)
self.editor_user = User.objects.create_user(
username='editor',
password='pass123'
)
self.admin_user = User.objects.create_superuser(
username='admin',
password='pass123'
)
# Add editor permission to editor_user
content_type = ContentType.objects.get_for_model(BlogPost)
edit_permission = Permission.objects.get(
codename='change_blogpost',
content_type=content_type
)
self.editor_user.user_permissions.add(edit_permission)
self.category = Category.objects.create(name='Tech', slug='tech')
self.post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=self.regular_user,
category=self.category
)
def test_admin_required_view_regular_user(self):
"""Test admin required view with regular user"""
self.client.login(username='regular', password='pass123')
url = reverse('admin:blog_blogpost_changelist')
response = self.client.get(url)
# Should redirect to admin login
self.assertEqual(response.status_code, 302)
def test_admin_required_view_admin_user(self):
"""Test admin required view with admin user"""
self.client.login(username='admin', password='pass123')
url = reverse('admin:blog_blogpost_changelist')
response = self.client.get(url)
# Should allow access
self.assertEqual(response.status_code, 200)
def test_permission_required_view_without_permission(self):
"""Test permission required view without permission"""
self.client.login(username='regular', password='pass123')
# Assuming a view that requires 'change_blogpost' permission
url = reverse('blog:post_edit_admin', kwargs={'pk': self.post.pk})
response = self.client.get(url)
# Should be forbidden
self.assertEqual(response.status_code, 403)
def test_permission_required_view_with_permission(self):
"""Test permission required view with permission"""
self.client.login(username='editor', password='pass123')
url = reverse('blog:post_edit_admin', kwargs={'pk': self.post.pk})
response = self.client.get(url)
# Should allow access
self.assertEqual(response.status_code, 200)
def test_superuser_access(self):
"""Test superuser access to all views"""
self.client.login(username='admin', password='pass123')
# Superuser should have access to all views
urls = [
reverse('blog:post_edit_admin', kwargs={'pk': self.post.pk}),
reverse('admin:blog_blogpost_changelist'),
]
for url in urls:
response = self.client.get(url)
self.assertIn(response.status_code, [200, 302]) # 302 for redirects
import json
from django.http import JsonResponse
class AjaxViewTests(TestCase):
"""Test AJAX views"""
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(name='Tech', slug='tech')
self.post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=self.user,
category=self.category
)
def test_ajax_like_post_authenticated(self):
"""Test AJAX like post view when authenticated"""
self.client.login(username='testuser', password='testpass123')
url = reverse('blog:ajax_like_post')
response = self.client.post(
url,
{'post_id': self.post.id},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
# Check response
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
# Parse JSON response
data = json.loads(response.content)
self.assertTrue(data['success'])
self.assertIn('likes_count', data)
def test_ajax_like_post_anonymous(self):
"""Test AJAX like post view when anonymous"""
url = reverse('blog:ajax_like_post')
response = self.client.post(
url,
{'post_id': self.post.id},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
# Should return error
self.assertEqual(response.status_code, 401)
data = json.loads(response.content)
self.assertFalse(data['success'])
self.assertIn('error', data)
def test_ajax_search_posts(self):
"""Test AJAX search posts view"""
# Create additional posts
BlogPost.objects.create(
title='Django Tutorial',
content='Learn Django',
author=self.user,
category=self.category
)
BlogPost.objects.create(
title='Python Guide',
content='Learn Python',
author=self.user,
category=self.category
)
url = reverse('blog:ajax_search_posts')
response = self.client.get(
url,
{'q': 'Django'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertIn('results', data)
self.assertEqual(len(data['results']), 1)
self.assertEqual(data['results'][0]['title'], 'Django Tutorial')
def test_non_ajax_request_to_ajax_view(self):
"""Test non-AJAX request to AJAX-only view"""
url = reverse('blog:ajax_like_post')
response = self.client.post(url, {'post_id': self.post.id})
# Should return bad request or redirect
self.assertIn(response.status_code, [400, 405])
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
class BlogAPITests(APITestCase):
"""Test REST API views"""
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(name='Tech', slug='tech')
self.post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=self.user,
category=self.category,
status='published'
)
def test_api_post_list_get(self):
"""Test API post list GET request"""
url = reverse('api:post-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
self.assertEqual(response.data['results'][0]['title'], 'Test Post')
def test_api_post_detail_get(self):
"""Test API post detail GET request"""
url = reverse('api:post-detail', kwargs={'pk': self.post.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Test Post')
self.assertEqual(response.data['author'], self.user.username)
def test_api_post_create_authenticated(self):
"""Test API post creation when authenticated"""
self.client.force_authenticate(user=self.user)
url = reverse('api:post-list')
data = {
'title': 'New API Post',
'content': 'Content created via API',
'category': self.category.id,
'status': 'published'
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['title'], 'New API Post')
# Check post was created in database
new_post = BlogPost.objects.get(title='New API Post')
self.assertEqual(new_post.author, self.user)
def test_api_post_create_unauthenticated(self):
"""Test API post creation when unauthenticated"""
url = reverse('api:post-list')
data = {
'title': 'Unauthorized Post',
'content': 'This should fail',
'category': self.category.id
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_api_post_update_owner(self):
"""Test API post update by owner"""
self.client.force_authenticate(user=self.user)
url = reverse('api:post-detail', kwargs={'pk': self.post.pk})
data = {
'title': 'Updated Title',
'content': self.post.content,
'category': self.category.id,
'status': self.post.status
}
response = self.client.put(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Updated Title')
# Check database was updated
self.post.refresh_from_db()
self.assertEqual(self.post.title, 'Updated Title')
def test_api_post_delete_owner(self):
"""Test API post deletion by owner"""
self.client.force_authenticate(user=self.user)
url = reverse('api:post-detail', kwargs={'pk': self.post.pk})
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
# Check post was deleted
self.assertFalse(BlogPost.objects.filter(pk=self.post.pk).exists())
class FormHandlingTests(TestCase):
"""Test form handling in views"""
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.category = Category.objects.create(name='Tech', slug='tech')
self.client.login(username='testuser', password='testpass123')
def test_contact_form_valid_submission(self):
"""Test contact form with valid data"""
url = reverse('blog:contact')
data = {
'name': 'John Doe',
'email': 'john@example.com',
'subject': 'Test Subject',
'message': 'This is a test message.'
}
response = self.client.post(url, data)
# Should redirect after successful submission
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, reverse('blog:contact_success'))
def test_contact_form_invalid_email(self):
"""Test contact form with invalid email"""
url = reverse('blog:contact')
data = {
'name': 'John Doe',
'email': 'invalid-email', # Invalid email format
'subject': 'Test Subject',
'message': 'This is a test message.'
}
response = self.client.post(url, data)
# Should return form with errors
self.assertEqual(response.status_code, 200)
self.assertFormError(
response,
'form',
'email',
'Enter a valid email address.'
)
def test_contact_form_missing_required_fields(self):
"""Test contact form with missing required fields"""
url = reverse('blog:contact')
data = {
'name': '', # Missing required field
'email': 'john@example.com',
'subject': '', # Missing required field
'message': 'This is a test message.'
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 200)
# Check multiple form errors
self.assertFormError(response, 'form', 'name', 'This field is required.')
self.assertFormError(response, 'form', 'subject', 'This field is required.')
def test_post_form_with_file_upload(self):
"""Test post form with file upload"""
from django.core.files.uploadedfile import SimpleUploadedFile
# Create test file
test_file = SimpleUploadedFile(
"test_image.jpg",
b"fake image content",
content_type="image/jpeg"
)
url = reverse('blog:post_create')
data = {
'title': 'Post with Image',
'content': 'This post has an image.',
'category': self.category.id,
'status': 'published',
'featured_image': test_file
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 302)
# Check post was created with image
post = BlogPost.objects.get(title='Post with Image')
self.assertTrue(post.featured_image)
def test_form_initial_data(self):
"""Test form with initial data"""
post = BlogPost.objects.create(
title='Existing Post',
content='Existing content',
author=self.user,
category=self.category
)
url = reverse('blog:post_update', kwargs={'slug': post.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# Check form is pre-populated
form = response.context['form']
self.assertEqual(form.initial['title'], 'Existing Post')
self.assertEqual(form.initial['content'], 'Existing content')
# views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
class OwnerRequiredMixin:
"""Mixin to ensure only object owner can access view"""
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
if obj.author != request.user:
raise PermissionDenied("You don't have permission to access this.")
return super().dispatch(request, *args, **kwargs)
class AjaxRequiredMixin:
"""Mixin to ensure view is only accessed via AJAX"""
def dispatch(self, request, *args, **kwargs):
if not request.is_ajax():
return JsonResponse({'error': 'AJAX required'}, status=400)
return super().dispatch(request, *args, **kwargs)
# tests.py
class ViewMixinTests(TestCase):
"""Test custom view mixins"""
def setUp(self):
self.user1 = User.objects.create_user('user1', 'user1@example.com', 'pass')
self.user2 = User.objects.create_user('user2', 'user2@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
self.post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=self.user1,
category=self.category
)
def test_owner_required_mixin_owner_access(self):
"""Test OwnerRequiredMixin allows owner access"""
self.client.login(username='user1', password='pass')
url = reverse('blog:post_update', kwargs={'slug': self.post.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_owner_required_mixin_non_owner_access(self):
"""Test OwnerRequiredMixin denies non-owner access"""
self.client.login(username='user2', password='pass')
url = reverse('blog:post_update', kwargs={'slug': self.post.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 403)
def test_ajax_required_mixin_ajax_request(self):
"""Test AjaxRequiredMixin allows AJAX requests"""
url = reverse('blog:ajax_view')
response = self.client.get(
url,
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assertEqual(response.status_code, 200)
def test_ajax_required_mixin_non_ajax_request(self):
"""Test AjaxRequiredMixin rejects non-AJAX requests"""
url = reverse('blog:ajax_view')
response = self.client.get(url)
self.assertEqual(response.status_code, 400)
data = json.loads(response.content)
self.assertIn('error', data)
class ErrorHandlingTests(TestCase):
"""Test error handling in views"""
def test_404_error_handling(self):
"""Test 404 error handling"""
url = '/nonexistent-page/'
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_500_error_handling(self):
"""Test 500 error handling"""
# This would test a view that intentionally raises an exception
# In practice, you'd mock a service to raise an exception
with self.assertRaises(Exception):
# Code that raises an exception
raise Exception("Test exception")
def test_custom_error_page_content(self):
"""Test custom error page content"""
# Assuming you have custom 404 template
url = '/nonexistent-page/'
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
self.assertContains(response, 'Page Not Found', status_code=404)
self.assertTemplateUsed(response, '404.html')
With comprehensive view testing in place, you're ready to move on to testing forms. The next chapter will cover testing Django forms, including field validation, custom validation methods, form rendering, and form processing logic.
Key view testing concepts covered:
View tests ensure your application's user interface works correctly and securely, providing confidence in your application's behavior from the user's perspective.
Testing Models
Model testing is fundamental to Django application quality. Models contain your application's core business logic, data validation rules, and database interactions. Comprehensive model testing ensures data integrity, validates business rules, and provides confidence when making changes to your data layer.
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.