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.
from django.test import TestCase
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.contrib.auth.models import User
from blog.models import BlogPost, Category, Tag
class BlogPostModelTests(TestCase):
"""Test BlogPost model functionality"""
def setUp(self):
"""Set up test data"""
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.category = Category.objects.create(
name='Technology',
slug='technology'
)
def test_post_creation_with_required_fields(self):
"""Test creating post with all required fields"""
post = BlogPost.objects.create(
title='Test Post',
content='This is test content.',
author=self.user,
category=self.category
)
# Verify post was created
self.assertEqual(post.title, 'Test Post')
self.assertEqual(post.content, 'This is test content.')
self.assertEqual(post.author, self.user)
self.assertEqual(post.category, self.category)
self.assertIsNotNone(post.created_at)
self.assertIsNotNone(post.updated_at)
def test_post_creation_without_required_fields_fails(self):
"""Test that creating post without required fields fails"""
# Test without title
with self.assertRaises(IntegrityError):
BlogPost.objects.create(
content='Content without title',
author=self.user,
category=self.category
)
# Test without author
with self.assertRaises(IntegrityError):
BlogPost.objects.create(
title='Post without author',
content='Content',
category=self.category
)
def test_post_str_representation(self):
"""Test string representation of post"""
post = BlogPost.objects.create(
title='Test Post Title',
content='Content',
author=self.user,
category=self.category
)
self.assertEqual(str(post), 'Test Post Title')
def test_post_slug_generation(self):
"""Test automatic slug generation from title"""
post = BlogPost.objects.create(
title='My First Blog Post!',
content='Content',
author=self.user,
category=self.category
)
self.assertEqual(post.slug, 'my-first-blog-post')
def test_post_slug_uniqueness(self):
"""Test that post slugs are unique"""
# Create first post
post1 = BlogPost.objects.create(
title='Test Post',
content='Content 1',
author=self.user,
category=self.category
)
# Create second post with same title
post2 = BlogPost.objects.create(
title='Test Post',
content='Content 2',
author=self.user,
category=self.category
)
# Slugs should be different
self.assertNotEqual(post1.slug, post2.slug)
self.assertEqual(post1.slug, 'test-post')
self.assertEqual(post2.slug, 'test-post-1') # Or similar unique variation
class BlogPostValidationTests(TestCase):
"""Test BlogPost field validation"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
def test_title_max_length_validation(self):
"""Test title maximum length validation"""
# Test valid length (assuming max_length=200)
valid_title = 'A' * 200
post = BlogPost(
title=valid_title,
content='Content',
author=self.user,
category=self.category
)
# Should not raise validation error
try:
post.full_clean()
except ValidationError:
self.fail("Valid title length should not raise ValidationError")
# Test invalid length
invalid_title = 'A' * 201
post = BlogPost(
title=invalid_title,
content='Content',
author=self.user,
category=self.category
)
with self.assertRaises(ValidationError) as cm:
post.full_clean()
self.assertIn('title', cm.exception.message_dict)
def test_content_blank_validation(self):
"""Test content field blank validation"""
# Test with empty content (if blank=False)
post = BlogPost(
title='Test Post',
content='', # Empty content
author=self.user,
category=self.category
)
# Depending on your model definition
with self.assertRaises(ValidationError):
post.full_clean()
def test_email_field_validation(self):
"""Test email field validation (if model has email field)"""
# Assuming UserProfile model with email field
from accounts.models import UserProfile
# Test valid email
profile = UserProfile(
user=self.user,
email='valid@example.com'
)
try:
profile.full_clean()
except ValidationError:
self.fail("Valid email should not raise ValidationError")
# Test invalid email
profile = UserProfile(
user=self.user,
email='invalid-email'
)
with self.assertRaises(ValidationError) as cm:
profile.full_clean()
self.assertIn('email', cm.exception.message_dict)
def test_positive_integer_validation(self):
"""Test positive integer field validation"""
# Assuming BlogPost has view_count field
post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=self.user,
category=self.category,
view_count=10
)
# Test valid positive value
post.view_count = 100
post.full_clean() # Should not raise error
# Test negative value
post.view_count = -1
with self.assertRaises(ValidationError):
post.full_clean()
def test_choice_field_validation(self):
"""Test choice field validation"""
# Test valid status
post = BlogPost(
title='Test Post',
content='Content',
author=self.user,
category=self.category,
status='published' # Valid choice
)
try:
post.full_clean()
except ValidationError:
self.fail("Valid status should not raise ValidationError")
# Test invalid status
post.status = 'invalid_status'
with self.assertRaises(ValidationError) as cm:
post.full_clean()
self.assertIn('status', cm.exception.message_dict)
class BlogPostMethodTests(TestCase):
"""Test BlogPost custom methods"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
def test_get_absolute_url(self):
"""Test get_absolute_url method"""
post = BlogPost.objects.create(
title='Test Post',
slug='test-post',
content='Content',
author=self.user,
category=self.category
)
expected_url = f'/blog/{post.slug}/'
self.assertEqual(post.get_absolute_url(), expected_url)
def test_is_published_method(self):
"""Test is_published method"""
# Test published post
published_post = BlogPost.objects.create(
title='Published Post',
content='Content',
author=self.user,
category=self.category,
status='published'
)
self.assertTrue(published_post.is_published())
# Test draft post
draft_post = BlogPost.objects.create(
title='Draft Post',
content='Content',
author=self.user,
category=self.category,
status='draft'
)
self.assertFalse(draft_post.is_published())
def test_get_reading_time(self):
"""Test reading time calculation"""
# Short content (less than 200 words)
short_content = 'Short content here.'
short_post = BlogPost.objects.create(
title='Short Post',
content=short_content,
author=self.user,
category=self.category
)
self.assertEqual(short_post.get_reading_time(), 1) # Minimum 1 minute
# Long content (400 words, assuming 200 words per minute)
long_content = ' '.join(['word'] * 400)
long_post = BlogPost.objects.create(
title='Long Post',
content=long_content,
author=self.user,
category=self.category
)
self.assertEqual(long_post.get_reading_time(), 2) # 2 minutes
def test_increment_view_count(self):
"""Test view count increment method"""
post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=self.user,
category=self.category,
view_count=0
)
# Test single increment
post.increment_view_count()
self.assertEqual(post.view_count, 1)
# Test multiple increments
for _ in range(5):
post.increment_view_count()
self.assertEqual(post.view_count, 6)
def test_get_related_posts(self):
"""Test related posts method"""
# Create tags
tag1 = Tag.objects.create(name='Python', slug='python')
tag2 = Tag.objects.create(name='Django', slug='django')
tag3 = Tag.objects.create(name='JavaScript', slug='javascript')
# Create main post
main_post = BlogPost.objects.create(
title='Main Post',
content='Content',
author=self.user,
category=self.category
)
main_post.tags.add(tag1, tag2)
# Create related posts
related_post1 = BlogPost.objects.create(
title='Related Post 1',
content='Content',
author=self.user,
category=self.category
)
related_post1.tags.add(tag1) # Shares Python tag
related_post2 = BlogPost.objects.create(
title='Related Post 2',
content='Content',
author=self.user,
category=self.category
)
related_post2.tags.add(tag2) # Shares Django tag
# Create unrelated post
unrelated_post = BlogPost.objects.create(
title='Unrelated Post',
content='Content',
author=self.user,
category=self.category
)
unrelated_post.tags.add(tag3) # No shared tags
# Test related posts
related_posts = main_post.get_related_posts()
self.assertIn(related_post1, related_posts)
self.assertIn(related_post2, related_posts)
self.assertNotIn(unrelated_post, related_posts)
self.assertNotIn(main_post, related_posts) # Shouldn't include itself
class BlogPostPropertyTests(TestCase):
"""Test BlogPost properties"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
def test_excerpt_property(self):
"""Test excerpt property"""
# Short content
short_content = 'This is short content.'
short_post = BlogPost.objects.create(
title='Short Post',
content=short_content,
author=self.user,
category=self.category
)
self.assertEqual(short_post.excerpt, short_content)
# Long content
long_content = 'This is a very long piece of content. ' * 20 # 160+ chars
long_post = BlogPost.objects.create(
title='Long Post',
content=long_content,
author=self.user,
category=self.category
)
excerpt = long_post.excerpt
self.assertLessEqual(len(excerpt), 150) # Assuming 150 char limit
self.assertTrue(excerpt.endswith('...'))
def test_word_count_property(self):
"""Test word count property"""
content = 'This content has exactly five words.'
post = BlogPost.objects.create(
title='Test Post',
content=content,
author=self.user,
category=self.category
)
self.assertEqual(post.word_count, 6) # "This content has exactly five words"
def test_is_recent_property(self):
"""Test is_recent property"""
from django.utils import timezone
from datetime import timedelta
# Recent post (created now)
recent_post = BlogPost.objects.create(
title='Recent Post',
content='Content',
author=self.user,
category=self.category
)
self.assertTrue(recent_post.is_recent)
# Old post (simulate old creation date)
old_post = BlogPost.objects.create(
title='Old Post',
content='Content',
author=self.user,
category=self.category
)
# Manually set old date
old_date = timezone.now() - timedelta(days=8) # Assuming 7 days is recent
BlogPost.objects.filter(id=old_post.id).update(created_at=old_date)
old_post.refresh_from_db()
self.assertFalse(old_post.is_recent)
# models.py
class PublishedPostManager(models.Manager):
"""Manager for published posts only"""
def get_queryset(self):
return super().get_queryset().filter(status='published')
def by_category(self, category):
return self.get_queryset().filter(category=category)
def recent(self, days=7):
from django.utils import timezone
from datetime import timedelta
cutoff_date = timezone.now() - timedelta(days=days)
return self.get_queryset().filter(created_at__gte=cutoff_date)
class BlogPost(models.Model):
# ... fields ...
objects = models.Manager() # Default manager
published = PublishedPostManager() # Custom manager
# tests.py
class BlogPostManagerTests(TestCase):
"""Test BlogPost custom managers"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
# Create published posts
self.published_post1 = BlogPost.objects.create(
title='Published Post 1',
content='Content',
author=self.user,
category=self.category,
status='published'
)
self.published_post2 = BlogPost.objects.create(
title='Published Post 2',
content='Content',
author=self.user,
category=self.category,
status='published'
)
# Create draft post
self.draft_post = BlogPost.objects.create(
title='Draft Post',
content='Content',
author=self.user,
category=self.category,
status='draft'
)
def test_published_manager_filters_published_only(self):
"""Test published manager returns only published posts"""
published_posts = BlogPost.published.all()
self.assertEqual(published_posts.count(), 2)
self.assertIn(self.published_post1, published_posts)
self.assertIn(self.published_post2, published_posts)
self.assertNotIn(self.draft_post, published_posts)
def test_published_manager_by_category(self):
"""Test published manager by_category method"""
# Create another category and post
other_category = Category.objects.create(name='Science', slug='science')
other_post = BlogPost.objects.create(
title='Science Post',
content='Content',
author=self.user,
category=other_category,
status='published'
)
# Test filtering by category
tech_posts = BlogPost.published.by_category(self.category)
science_posts = BlogPost.published.by_category(other_category)
self.assertEqual(tech_posts.count(), 2)
self.assertEqual(science_posts.count(), 1)
self.assertIn(other_post, science_posts)
self.assertNotIn(other_post, tech_posts)
def test_published_manager_recent_method(self):
"""Test published manager recent method"""
from django.utils import timezone
from datetime import timedelta
# Create old post
old_post = BlogPost.objects.create(
title='Old Post',
content='Content',
author=self.user,
category=self.category,
status='published'
)
# Set old date
old_date = timezone.now() - timedelta(days=10)
BlogPost.objects.filter(id=old_post.id).update(created_at=old_date)
# Test recent posts (default 7 days)
recent_posts = BlogPost.published.recent()
self.assertEqual(recent_posts.count(), 2) # Only recent published posts
self.assertNotIn(old_post, recent_posts)
# Test custom days parameter
recent_posts_15_days = BlogPost.published.recent(days=15)
self.assertEqual(recent_posts_15_days.count(), 3) # Includes old post
self.assertIn(old_post, recent_posts_15_days)
# models.py
class BlogPostQuerySet(models.QuerySet):
"""Custom QuerySet for BlogPost"""
def published(self):
return self.filter(status='published')
def by_author(self, author):
return self.filter(author=author)
def search(self, query):
return self.filter(
models.Q(title__icontains=query) |
models.Q(content__icontains=query)
)
def with_tags(self, *tag_names):
return self.filter(tags__name__in=tag_names).distinct()
class BlogPost(models.Model):
# ... fields ...
objects = BlogPostQuerySet.as_manager()
# tests.py
class BlogPostQuerySetTests(TestCase):
"""Test BlogPost custom QuerySet methods"""
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')
# Create test posts
self.post1 = BlogPost.objects.create(
title='Django Tutorial',
content='Learn Django framework',
author=self.user1,
category=self.category,
status='published'
)
self.post2 = BlogPost.objects.create(
title='Python Guide',
content='Python programming guide',
author=self.user2,
category=self.category,
status='published'
)
self.post3 = BlogPost.objects.create(
title='Draft Post',
content='This is a draft',
author=self.user1,
category=self.category,
status='draft'
)
def test_published_queryset_method(self):
"""Test published QuerySet method"""
published_posts = BlogPost.objects.published()
self.assertEqual(published_posts.count(), 2)
self.assertIn(self.post1, published_posts)
self.assertIn(self.post2, published_posts)
self.assertNotIn(self.post3, published_posts)
def test_by_author_queryset_method(self):
"""Test by_author QuerySet method"""
user1_posts = BlogPost.objects.by_author(self.user1)
user2_posts = BlogPost.objects.by_author(self.user2)
self.assertEqual(user1_posts.count(), 2) # post1 and post3
self.assertEqual(user2_posts.count(), 1) # post2
self.assertIn(self.post1, user1_posts)
self.assertIn(self.post3, user1_posts)
self.assertIn(self.post2, user2_posts)
def test_search_queryset_method(self):
"""Test search QuerySet method"""
# Search in title
django_posts = BlogPost.objects.search('Django')
self.assertEqual(django_posts.count(), 1)
self.assertIn(self.post1, django_posts)
# Search in content
python_posts = BlogPost.objects.search('Python')
self.assertEqual(python_posts.count(), 1)
self.assertIn(self.post2, python_posts)
# Search with no results
no_results = BlogPost.objects.search('nonexistent')
self.assertEqual(no_results.count(), 0)
def test_chained_queryset_methods(self):
"""Test chaining custom QuerySet methods"""
# Chain published and by_author
user1_published = BlogPost.objects.published().by_author(self.user1)
self.assertEqual(user1_published.count(), 1)
self.assertIn(self.post1, user1_published)
self.assertNotIn(self.post3, user1_published) # Draft, not published
# Chain search and published
published_django = BlogPost.objects.search('Django').published()
self.assertEqual(published_django.count(), 1)
self.assertIn(self.post1, published_django)
def test_with_tags_queryset_method(self):
"""Test with_tags QuerySet method"""
# Create tags
django_tag = Tag.objects.create(name='Django', slug='django')
python_tag = Tag.objects.create(name='Python', slug='python')
web_tag = Tag.objects.create(name='Web', slug='web')
# Add tags to posts
self.post1.tags.add(django_tag, web_tag)
self.post2.tags.add(python_tag)
# Test single tag
django_posts = BlogPost.objects.with_tags('Django')
self.assertEqual(django_posts.count(), 1)
self.assertIn(self.post1, django_posts)
# Test multiple tags
web_or_python_posts = BlogPost.objects.with_tags('Web', 'Python')
self.assertEqual(web_or_python_posts.count(), 2)
self.assertIn(self.post1, web_or_python_posts)
self.assertIn(self.post2, web_or_python_posts)
class RelationshipTests(TestCase):
"""Test model relationships"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
def test_post_author_relationship(self):
"""Test post-author foreign key relationship"""
post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=self.user,
category=self.category
)
# Test forward relationship
self.assertEqual(post.author, self.user)
self.assertEqual(post.author.username, 'testuser')
# Test reverse relationship
user_posts = self.user.blogpost_set.all()
self.assertEqual(user_posts.count(), 1)
self.assertIn(post, user_posts)
def test_post_category_relationship(self):
"""Test post-category foreign key relationship"""
post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=self.user,
category=self.category
)
# Test forward relationship
self.assertEqual(post.category, self.category)
self.assertEqual(post.category.name, 'Tech')
# Test reverse relationship
category_posts = self.category.blogpost_set.all()
self.assertEqual(category_posts.count(), 1)
self.assertIn(post, category_posts)
def test_cascade_deletion(self):
"""Test cascade deletion behavior"""
post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=self.user,
category=self.category
)
post_id = post.id
# Delete user (assuming CASCADE)
self.user.delete()
# Post should be deleted too
with self.assertRaises(BlogPost.DoesNotExist):
BlogPost.objects.get(id=post_id)
class ManyToManyTests(TestCase):
"""Test many-to-many relationships"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
# Create tags
self.tag1 = Tag.objects.create(name='Python', slug='python')
self.tag2 = Tag.objects.create(name='Django', slug='django')
self.tag3 = Tag.objects.create(name='Web', slug='web')
# Create post
self.post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=self.user,
category=self.category
)
def test_adding_tags_to_post(self):
"""Test adding tags to post"""
# Add single tag
self.post.tags.add(self.tag1)
self.assertEqual(self.post.tags.count(), 1)
self.assertIn(self.tag1, self.post.tags.all())
# Add multiple tags
self.post.tags.add(self.tag2, self.tag3)
self.assertEqual(self.post.tags.count(), 3)
self.assertIn(self.tag2, self.post.tags.all())
self.assertIn(self.tag3, self.post.tags.all())
def test_removing_tags_from_post(self):
"""Test removing tags from post"""
# Add tags first
self.post.tags.add(self.tag1, self.tag2, self.tag3)
self.assertEqual(self.post.tags.count(), 3)
# Remove single tag
self.post.tags.remove(self.tag1)
self.assertEqual(self.post.tags.count(), 2)
self.assertNotIn(self.tag1, self.post.tags.all())
# Clear all tags
self.post.tags.clear()
self.assertEqual(self.post.tags.count(), 0)
def test_reverse_many_to_many_relationship(self):
"""Test reverse many-to-many relationship"""
# Create another post
post2 = BlogPost.objects.create(
title='Another Post',
content='Content',
author=self.user,
category=self.category
)
# Add same tag to both posts
self.post.tags.add(self.tag1)
post2.tags.add(self.tag1)
# Test reverse relationship
tag_posts = self.tag1.blogpost_set.all()
self.assertEqual(tag_posts.count(), 2)
self.assertIn(self.post, tag_posts)
self.assertIn(post2, tag_posts)
def test_many_to_many_with_through_model(self):
"""Test many-to-many with through model"""
# Assuming PostTag through model with additional fields
from blog.models import PostTag
# Create relationship with additional data
post_tag = PostTag.objects.create(
post=self.post,
tag=self.tag1,
added_by=self.user,
weight=5
)
# Test relationship exists
self.assertTrue(
self.post.tags.filter(id=self.tag1.id).exists()
)
# Test through model data
through_obj = PostTag.objects.get(post=self.post, tag=self.tag1)
self.assertEqual(through_obj.added_by, self.user)
self.assertEqual(through_obj.weight, 5)
# models.py
from django.core.exceptions import ValidationError
def validate_post_content_length(value):
"""Custom validator for post content"""
if len(value.split()) < 10:
raise ValidationError('Post content must have at least 10 words.')
class BlogPost(models.Model):
# ... other fields ...
content = models.TextField(validators=[validate_post_content_length])
# tests.py
class ValidationTests(TestCase):
"""Test custom validation"""
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.category = Category.objects.create(name='Tech', slug='tech')
def test_content_length_validation_passes(self):
"""Test content validation passes with sufficient words"""
valid_content = 'This is a valid post content with more than ten words in it.'
post = BlogPost(
title='Test Post',
content=valid_content,
author=self.user,
category=self.category
)
# Should not raise validation error
try:
post.full_clean()
except ValidationError:
self.fail("Valid content should not raise ValidationError")
def test_content_length_validation_fails(self):
"""Test content validation fails with insufficient words"""
invalid_content = 'Too short content here.' # Only 4 words
post = BlogPost(
title='Test Post',
content=invalid_content,
author=self.user,
category=self.category
)
with self.assertRaises(ValidationError) as cm:
post.full_clean()
self.assertIn('content', cm.exception.message_dict)
self.assertIn('at least 10 words', str(cm.exception))
class ConstraintTests(TestCase):
"""Test database constraints"""
def test_unique_constraint(self):
"""Test unique constraint on slug field"""
user = User.objects.create_user('testuser', 'test@example.com', 'pass')
category = Category.objects.create(name='Tech', slug='tech')
# Create first post
BlogPost.objects.create(
title='Test Post',
slug='test-post',
content='Content',
author=user,
category=category
)
# Try to create second post with same slug
with self.assertRaises(IntegrityError):
BlogPost.objects.create(
title='Another Test Post',
slug='test-post', # Same slug
content='Different content',
author=user,
category=category
)
def test_check_constraint(self):
"""Test check constraint (if using database check constraints)"""
# Assuming a check constraint that view_count >= 0
user = User.objects.create_user('testuser', 'test@example.com', 'pass')
category = Category.objects.create(name='Tech', slug='tech')
# This should work
post = BlogPost.objects.create(
title='Test Post',
content='Content',
author=user,
category=category,
view_count=0
)
# This should fail (if check constraint exists)
with self.assertRaises(IntegrityError):
post.view_count = -1
post.save()
With comprehensive model testing in place, you're ready to move on to testing views. The next chapter will cover testing Django views, including URL routing, request handling, response validation, and user authentication scenarios.
Key model testing concepts covered:
Model tests form the foundation of your test suite, ensuring data integrity and business rule enforcement throughout your application.
Test Tools
Django provides a comprehensive suite of testing tools that make it easier to write effective tests for web applications. These tools include utilities for creating test data, mocking external dependencies, measuring test coverage, and debugging test failures.
Testing Views
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.