Testing

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 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.

Basic Model Testing

Testing Model Creation and Fields

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

Testing Field Validation

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)

Testing Model Methods

Testing Custom Model Methods

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

Testing Model Properties

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)

Testing Model Managers and QuerySets

Testing Custom Managers

# 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)

Testing Custom QuerySets

# 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)

Testing Model Relationships

Testing Foreign Key Relationships

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)

Testing Many-to-Many Relationships

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)

Testing Model Validation and Constraints

Testing Custom Validation

# 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))

Testing Database Constraints

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()

Next Steps

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:

  • Basic model creation and field validation
  • Custom model methods and properties
  • Model managers and custom QuerySets
  • Relationship testing (ForeignKey, ManyToMany)
  • Custom validation and database constraints
  • Business logic validation

Model tests form the foundation of your test suite, ensuring data integrity and business rule enforcement throughout your application.