Testing

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

Basic Form Testing

Testing Form Validation

from django.test import TestCase
from django.core.exceptions import ValidationError
from blog.forms import BlogPostForm, CommentForm, ContactForm
from blog.models import BlogPost, Category
from django.contrib.auth.models import User

class BlogPostFormTests(TestCase):
    """Test BlogPost form validation"""
    
    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_form_valid_data(self):
        """Test form with valid data"""
        
        form_data = {
            'title': 'Test Blog Post',
            'content': 'This is test content for the blog post.',
            'category': self.category.id,
            'status': 'published',
            'tags': 'django, python, web'
        }
        
        form = BlogPostForm(data=form_data)
        
        # Form should be valid
        self.assertTrue(form.is_valid())
        
        # Check cleaned data
        self.assertEqual(form.cleaned_data['title'], 'Test Blog Post')
        self.assertEqual(form.cleaned_data['category'], self.category)
        self.assertEqual(form.cleaned_data['status'], 'published')
    
    def test_form_missing_required_fields(self):
        """Test form with missing required fields"""
        
        form_data = {
            'content': 'Content without title',
            'category': self.category.id
        }
        
        form = BlogPostForm(data=form_data)
        
        # Form should be invalid
        self.assertFalse(form.is_valid())
        
        # Check specific field errors
        self.assertIn('title', form.errors)
        self.assertEqual(form.errors['title'], ['This field is required.'])
    
    def test_form_invalid_field_data(self):
        """Test form with invalid field data"""
        
        form_data = {
            'title': 'A' * 201,  # Assuming max_length=200
            'content': 'Valid content',
            'category': 999,  # Non-existent category
            'status': 'invalid_status'  # Invalid choice
        }
        
        form = BlogPostForm(data=form_data)
        
        self.assertFalse(form.is_valid())
        
        # Check multiple field errors
        self.assertIn('title', form.errors)
        self.assertIn('category', form.errors)
        self.assertIn('status', form.errors)
    
    def test_form_empty_data(self):
        """Test form with empty data"""
        
        form = BlogPostForm(data={})
        
        self.assertFalse(form.is_valid())
        
        # All required fields should have errors
        required_fields = ['title', 'content', 'category']
        for field in required_fields:
            self.assertIn(field, form.errors)
    
    def test_form_save_method(self):
        """Test form save method"""
        
        form_data = {
            'title': 'Test Blog Post',
            'content': 'This is test content.',
            'category': self.category.id,
            'status': 'published'
        }
        
        form = BlogPostForm(data=form_data)
        
        self.assertTrue(form.is_valid())
        
        # Save form (assuming form has save method)
        post = form.save(commit=False)
        post.author = self.user
        post.save()
        
        # Verify post was created
        self.assertEqual(post.title, 'Test Blog Post')
        self.assertEqual(post.author, self.user)
        self.assertEqual(post.category, self.category)
        
        # Verify post exists in database
        saved_post = BlogPost.objects.get(title='Test Blog Post')
        self.assertEqual(saved_post.author, self.user)

Testing Custom Form Validation

# forms.py
from django import forms
from django.core.exceptions import ValidationError
import re

class BlogPostForm(forms.ModelForm):
    """Blog post form with custom validation"""
    
    tags = forms.CharField(
        max_length=200,
        help_text='Enter tags separated by commas',
        required=False
    )
    
    class Meta:
        model = BlogPost
        fields = ['title', 'content', 'category', 'status', 'tags']
    
    def clean_title(self):
        """Custom validation for title field"""
        title = self.cleaned_data.get('title')
        
        if title:
            # Check for profanity (simplified example)
            profanity_words = ['spam', 'fake', 'scam']
            if any(word in title.lower() for word in profanity_words):
                raise ValidationError('Title contains inappropriate content.')
            
            # Check for minimum word count
            if len(title.split()) < 3:
                raise ValidationError('Title must contain at least 3 words.')
        
        return title
    
    def clean_content(self):
        """Custom validation for content field"""
        content = self.cleaned_data.get('content')
        
        if content:
            # Check minimum word count
            word_count = len(content.split())
            if word_count < 50:
                raise ValidationError(
                    f'Content must contain at least 50 words. Current: {word_count}'
                )
            
            # Check for HTML tags (if not allowed)
            if re.search(r'<[^>]+>', content):
                raise ValidationError('HTML tags are not allowed in content.')
        
        return content
    
    def clean_tags(self):
        """Custom validation for tags field"""
        tags = self.cleaned_data.get('tags')
        
        if tags:
            tag_list = [tag.strip() for tag in tags.split(',')]
            
            # Check maximum number of tags
            if len(tag_list) > 5:
                raise ValidationError('Maximum 5 tags allowed.')
            
            # Check tag length
            for tag in tag_list:
                if len(tag) > 20:
                    raise ValidationError(f'Tag "{tag}" is too long (max 20 characters).')
        
        return tags
    
    def clean(self):
        """Cross-field validation"""
        cleaned_data = super().clean()
        title = cleaned_data.get('title')
        content = cleaned_data.get('content')
        status = cleaned_data.get('status')
        
        # Check that published posts have sufficient content
        if status == 'published':
            if not title or len(title.split()) < 3:
                raise ValidationError('Published posts must have a proper title.')
            
            if not content or len(content.split()) < 100:
                raise ValidationError('Published posts must have at least 100 words.')
        
        return cleaned_data

# tests.py
class CustomValidationTests(TestCase):
    """Test custom form validation"""
    
    def setUp(self):
        self.category = Category.objects.create(name='Tech', slug='tech')
    
    def test_title_profanity_validation(self):
        """Test title profanity validation"""
        
        form_data = {
            'title': 'This is a spam post',  # Contains profanity
            'content': 'Valid content with more than fifty words. ' * 10,
            'category': self.category.id,
            'status': 'draft'
        }
        
        form = BlogPostForm(data=form_data)
        
        self.assertFalse(form.is_valid())
        self.assertIn('title', form.errors)
        self.assertIn('inappropriate content', str(form.errors['title']))
    
    def test_title_minimum_words_validation(self):
        """Test title minimum words validation"""
        
        form_data = {
            'title': 'Short',  # Only 1 word
            'content': 'Valid content with more than fifty words. ' * 10,
            'category': self.category.id,
            'status': 'draft'
        }
        
        form = BlogPostForm(data=form_data)
        
        self.assertFalse(form.is_valid())
        self.assertIn('title', form.errors)
        self.assertIn('at least 3 words', str(form.errors['title']))
    
    def test_content_minimum_words_validation(self):
        """Test content minimum words validation"""
        
        form_data = {
            'title': 'Valid Blog Post Title',
            'content': 'Too short content.',  # Less than 50 words
            'category': self.category.id,
            'status': 'draft'
        }
        
        form = BlogPostForm(data=form_data)
        
        self.assertFalse(form.is_valid())
        self.assertIn('content', form.errors)
        self.assertIn('at least 50 words', str(form.errors['content']))
    
    def test_content_html_validation(self):
        """Test content HTML tags validation"""
        
        form_data = {
            'title': 'Valid Blog Post Title',
            'content': 'Content with <script>alert("xss")</script> HTML tags. ' * 10,
            'category': self.category.id,
            'status': 'draft'
        }
        
        form = BlogPostForm(data=form_data)
        
        self.assertFalse(form.is_valid())
        self.assertIn('content', form.errors)
        self.assertIn('HTML tags are not allowed', str(form.errors['content']))
    
    def test_tags_maximum_count_validation(self):
        """Test tags maximum count validation"""
        
        form_data = {
            'title': 'Valid Blog Post Title',
            'content': 'Valid content with more than fifty words. ' * 10,
            'category': self.category.id,
            'status': 'draft',
            'tags': 'tag1, tag2, tag3, tag4, tag5, tag6'  # 6 tags (max 5)
        }
        
        form = BlogPostForm(data=form_data)
        
        self.assertFalse(form.is_valid())
        self.assertIn('tags', form.errors)
        self.assertIn('Maximum 5 tags', str(form.errors['tags']))
    
    def test_tags_length_validation(self):
        """Test individual tag length validation"""
        
        form_data = {
            'title': 'Valid Blog Post Title',
            'content': 'Valid content with more than fifty words. ' * 10,
            'category': self.category.id,
            'status': 'draft',
            'tags': 'short, verylongtagthatexceedstwentycharacters'
        }
        
        form = BlogPostForm(data=form_data)
        
        self.assertFalse(form.is_valid())
        self.assertIn('tags', form.errors)
        self.assertIn('too long', str(form.errors['tags']))
    
    def test_cross_field_validation_published_post(self):
        """Test cross-field validation for published posts"""
        
        form_data = {
            'title': 'Short',  # Invalid for published post
            'content': 'Short content.',  # Invalid for published post
            'category': self.category.id,
            'status': 'published'
        }
        
        form = BlogPostForm(data=form_data)
        
        self.assertFalse(form.is_valid())
        self.assertIn('__all__', form.errors)  # Non-field errors
        
        # Check both validation errors
        non_field_errors = form.non_field_errors()
        self.assertTrue(
            any('proper title' in str(error) for error in non_field_errors)
        )
        self.assertTrue(
            any('100 words' in str(error) for error in non_field_errors)
        )
    
    def test_valid_form_passes_all_validation(self):
        """Test that valid form passes all custom validation"""
        
        form_data = {
            'title': 'This is a Valid Blog Post Title',
            'content': 'This is valid content with more than fifty words. ' * 10,
            'category': self.category.id,
            'status': 'published',
            'tags': 'django, python, web, tutorial'
        }
        
        form = BlogPostForm(data=form_data)
        
        self.assertTrue(form.is_valid())
        self.assertEqual(len(form.errors), 0)

Testing Form Fields and Widgets

Testing Custom Form Fields

# forms.py
class TagField(forms.CharField):
    """Custom field for handling tags"""
    
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('help_text', 'Enter tags separated by commas')
        super().__init__(*args, **kwargs)
    
    def to_python(self, value):
        """Convert string to list of tags"""
        if not value:
            return []
        
        # Split by comma and clean up
        tags = [tag.strip().lower() for tag in value.split(',')]
        return [tag for tag in tags if tag]  # Remove empty tags
    
    def validate(self, value):
        """Validate tag list"""
        super().validate(value)
        
        if value:
            # Check for duplicate tags
            if len(value) != len(set(value)):
                raise ValidationError('Duplicate tags are not allowed.')
            
            # Check individual tag format
            for tag in value:
                if not re.match(r'^[a-zA-Z0-9-_]+$', tag):
                    raise ValidationError(
                        f'Tag "{tag}" contains invalid characters. '
                        'Use only letters, numbers, hyphens, and underscores.'
                    )

# Custom widget
class TagWidget(forms.TextInput):
    """Custom widget for tag input with JavaScript enhancement"""
    
    def __init__(self, attrs=None):
        default_attrs = {
            'class': 'tag-input',
            'placeholder': 'Enter tags separated by commas'
        }
        if attrs:
            default_attrs.update(attrs)
        super().__init__(default_attrs)

# tests.py
class CustomFieldTests(TestCase):
    """Test custom form fields"""
    
    def test_tag_field_to_python_valid_input(self):
        """Test TagField to_python method with valid input"""
        
        field = TagField()
        
        # Test normal input
        result = field.to_python('django, python, web')
        self.assertEqual(result, ['django', 'python', 'web'])
        
        # Test with extra spaces
        result = field.to_python('  django  ,  python  ,  web  ')
        self.assertEqual(result, ['django', 'python', 'web'])
        
        # Test empty input
        result = field.to_python('')
        self.assertEqual(result, [])
        
        # Test None input
        result = field.to_python(None)
        self.assertEqual(result, [])
    
    def test_tag_field_validation_duplicate_tags(self):
        """Test TagField validation with duplicate tags"""
        
        field = TagField()
        
        with self.assertRaises(ValidationError) as cm:
            field.validate(['django', 'python', 'django'])
        
        self.assertIn('Duplicate tags', str(cm.exception))
    
    def test_tag_field_validation_invalid_characters(self):
        """Test TagField validation with invalid characters"""
        
        field = TagField()
        
        with self.assertRaises(ValidationError) as cm:
            field.validate(['valid-tag', 'invalid tag!'])
        
        self.assertIn('invalid characters', str(cm.exception))
        self.assertIn('invalid tag!', str(cm.exception))
    
    def test_tag_field_validation_valid_tags(self):
        """Test TagField validation with valid tags"""
        
        field = TagField()
        
        # Should not raise exception
        try:
            field.validate(['django', 'python-3', 'web_dev', 'api2'])
        except ValidationError:
            self.fail("Valid tags should not raise ValidationError")
    
    def test_tag_widget_attributes(self):
        """Test TagWidget attributes"""
        
        widget = TagWidget()
        
        # Check default attributes
        self.assertIn('tag-input', widget.attrs['class'])
        self.assertIn('Enter tags', widget.attrs['placeholder'])
        
        # Test custom attributes
        custom_widget = TagWidget(attrs={'class': 'custom-class'})
        self.assertEqual(custom_widget.attrs['class'], 'custom-class')

Testing Form Rendering

class FormRenderingTests(TestCase):
    """Test form rendering and HTML output"""
    
    def setUp(self):
        self.category = Category.objects.create(name='Tech', slug='tech')
    
    def test_form_as_p_rendering(self):
        """Test form rendering as paragraphs"""
        
        form = BlogPostForm()
        html = form.as_p()
        
        # Check that form fields are rendered
        self.assertIn('<input', html)
        self.assertIn('name="title"', html)
        self.assertIn('<textarea', html)
        self.assertIn('name="content"', html)
        self.assertIn('<select', html)
        self.assertIn('name="category"', html)
    
    def test_form_field_help_text(self):
        """Test form field help text rendering"""
        
        form = BlogPostForm()
        
        # Check help text is included
        html = str(form['tags'])
        self.assertIn('Enter tags separated by commas', html)
    
    def test_form_field_errors_rendering(self):
        """Test form field errors rendering"""
        
        form_data = {
            'title': '',  # Invalid: required field
            'content': 'Short',  # Invalid: too short
            'category': self.category.id
        }
        
        form = BlogPostForm(data=form_data)
        self.assertFalse(form.is_valid())
        
        # Check error rendering
        title_html = str(form['title'])
        self.assertIn('errorlist', form.errors['title'].as_ul())
        
        # Check non-field errors
        if form.non_field_errors():
            errors_html = form.non_field_errors().as_ul()
            self.assertIn('errorlist', errors_html)
    
    def test_form_initial_data_rendering(self):
        """Test form rendering with initial data"""
        
        initial_data = {
            'title': 'Initial Title',
            'content': 'Initial content',
            'status': 'draft'
        }
        
        form = BlogPostForm(initial=initial_data)
        
        # Check initial values are rendered
        title_html = str(form['title'])
        self.assertIn('value="Initial Title"', title_html)
        
        content_html = str(form['content'])
        self.assertIn('Initial content', content_html)
    
    def test_form_css_classes(self):
        """Test form CSS classes"""
        
        class StyledBlogPostForm(BlogPostForm):
            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                
                # Add CSS classes to fields
                self.fields['title'].widget.attrs.update({
                    'class': 'form-control title-input'
                })
                self.fields['content'].widget.attrs.update({
                    'class': 'form-control content-textarea'
                })
        
        form = StyledBlogPostForm()
        
        # Check CSS classes are applied
        title_html = str(form['title'])
        self.assertIn('class="form-control title-input"', title_html)
        
        content_html = str(form['content'])
        self.assertIn('class="form-control content-textarea"', content_html)

Testing ModelForms

Testing ModelForm Integration

class ModelFormTests(TestCase):
    """Test ModelForm integration with models"""
    
    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_modelform_save_creates_instance(self):
        """Test ModelForm save creates model instance"""
        
        form_data = {
            'title': 'Test Post',
            'content': 'This is test content with more than fifty words. ' * 10,
            'category': self.category.id,
            'status': 'published'
        }
        
        form = BlogPostForm(data=form_data)
        self.assertTrue(form.is_valid())
        
        # Save without committing to add author
        post = form.save(commit=False)
        post.author = self.user
        post.save()
        
        # Verify instance was created
        self.assertIsInstance(post, BlogPost)
        self.assertEqual(post.title, 'Test Post')
        self.assertEqual(post.author, self.user)
        
        # Verify instance exists in database
        saved_post = BlogPost.objects.get(title='Test Post')
        self.assertEqual(saved_post.id, post.id)
    
    def test_modelform_save_updates_instance(self):
        """Test ModelForm save updates existing instance"""
        
        # Create existing post
        post = BlogPost.objects.create(
            title='Original Title',
            content='Original content with more than fifty words. ' * 10,
            author=self.user,
            category=self.category
        )
        
        # Update via form
        form_data = {
            'title': 'Updated Title',
            'content': 'Updated content with more than fifty words. ' * 10,
            'category': self.category.id,
            'status': 'published'
        }
        
        form = BlogPostForm(data=form_data, instance=post)
        self.assertTrue(form.is_valid())
        
        updated_post = form.save()
        
        # Verify update
        self.assertEqual(updated_post.id, post.id)  # Same instance
        self.assertEqual(updated_post.title, 'Updated Title')
        
        # Verify in database
        post.refresh_from_db()
        self.assertEqual(post.title, 'Updated Title')
    
    def test_modelform_exclude_fields(self):
        """Test ModelForm with excluded fields"""
        
        class LimitedBlogPostForm(forms.ModelForm):
            class Meta:
                model = BlogPost
                fields = ['title', 'content']  # Exclude category, status, etc.
        
        form_data = {
            'title': 'Test Post',
            'content': 'Test content with more than fifty words. ' * 10
        }
        
        form = LimitedBlogPostForm(data=form_data)
        self.assertTrue(form.is_valid())
        
        # Save with additional required fields
        post = form.save(commit=False)
        post.author = self.user
        post.category = self.category
        post.save()
        
        self.assertEqual(post.title, 'Test Post')
        self.assertEqual(post.author, self.user)
        self.assertEqual(post.category, self.category)
    
    def test_modelform_custom_field_override(self):
        """Test ModelForm with custom field override"""
        
        class CustomBlogPostForm(forms.ModelForm):
            # Override content field with custom widget
            content = forms.CharField(
                widget=forms.Textarea(attrs={'rows': 10, 'cols': 80}),
                help_text='Enter your blog post content here'
            )
            
            class Meta:
                model = BlogPost
                fields = ['title', 'content', 'category', 'status']
        
        form = CustomBlogPostForm()
        
        # Check custom widget attributes
        content_field = form.fields['content']
        self.assertEqual(content_field.widget.attrs['rows'], 10)
        self.assertEqual(content_field.widget.attrs['cols'], 80)
        self.assertEqual(content_field.help_text, 'Enter your blog post content here')

Testing Form Inheritance

Testing Form Class Inheritance

# forms.py
class BasePostForm(forms.ModelForm):
    """Base form for post-related forms"""
    
    class Meta:
        model = BlogPost
        fields = ['title', 'content']
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Common field customizations
        self.fields['title'].widget.attrs.update({
            'class': 'form-control',
            'placeholder': 'Enter post title'
        })
        
        self.fields['content'].widget.attrs.update({
            'class': 'form-control',
            'rows': 8
        })
    
    def clean_title(self):
        """Common title validation"""
        title = self.cleaned_data.get('title')
        if title and len(title.split()) < 2:
            raise ValidationError('Title must contain at least 2 words.')
        return title

class BlogPostCreateForm(BasePostForm):
    """Form for creating new blog posts"""
    
    class Meta(BasePostForm.Meta):
        fields = BasePostForm.Meta.fields + ['category', 'status', 'tags']
    
    def clean(self):
        """Additional validation for new posts"""
        cleaned_data = super().clean()
        
        # New posts must have category
        if not cleaned_data.get('category'):
            raise ValidationError('Category is required for new posts.')
        
        return cleaned_data

class BlogPostUpdateForm(BasePostForm):
    """Form for updating existing blog posts"""
    
    class Meta(BasePostForm.Meta):
        fields = BasePostForm.Meta.fields + ['category', 'status']
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Make status field optional for updates
        self.fields['status'].required = False

# tests.py
class FormInheritanceTests(TestCase):
    """Test form inheritance"""
    
    def setUp(self):
        self.category = Category.objects.create(name='Tech', slug='tech')
    
    def test_base_form_functionality(self):
        """Test base form functionality"""
        
        form_data = {
            'title': 'Test Post Title',
            'content': 'Test content for the post.'
        }
        
        form = BasePostForm(data=form_data)
        self.assertTrue(form.is_valid())
        
        # Check base validation works
        form_data['title'] = 'Short'  # Only 1 word
        form = BasePostForm(data=form_data)
        self.assertFalse(form.is_valid())
        self.assertIn('title', form.errors)
    
    def test_create_form_inheritance(self):
        """Test create form inherits from base form"""
        
        form_data = {
            'title': 'Test Post Title',
            'content': 'Test content for the post.',
            'category': self.category.id,
            'status': 'draft'
        }
        
        form = BlogPostCreateForm(data=form_data)
        self.assertTrue(form.is_valid())
        
        # Check inherited validation
        form_data['title'] = 'Short'
        form = BlogPostCreateForm(data=form_data)
        self.assertFalse(form.is_valid())
        self.assertIn('title', form.errors)  # Base form validation
        
        # Check additional validation
        form_data['title'] = 'Valid Post Title'
        form_data['category'] = None
        form = BlogPostCreateForm(data=form_data)
        self.assertFalse(form.is_valid())
        self.assertIn('Category is required', str(form.non_field_errors()))
    
    def test_update_form_inheritance(self):
        """Test update form inherits from base form"""
        
        form_data = {
            'title': 'Updated Post Title',
            'content': 'Updated content for the post.',
            'category': self.category.id
            # status is optional for updates
        }
        
        form = BlogPostUpdateForm(data=form_data)
        self.assertTrue(form.is_valid())
        
        # Check status is optional
        self.assertFalse(form.fields['status'].required)
    
    def test_form_field_inheritance(self):
        """Test form field attributes are inherited"""
        
        create_form = BlogPostCreateForm()
        update_form = BlogPostUpdateForm()
        
        # Check inherited field attributes
        for form in [create_form, update_form]:
            title_attrs = form.fields['title'].widget.attrs
            self.assertIn('form-control', title_attrs['class'])
            self.assertEqual(title_attrs['placeholder'], 'Enter post title')
            
            content_attrs = form.fields['content'].widget.attrs
            self.assertIn('form-control', content_attrs['class'])
            self.assertEqual(content_attrs['rows'], 8)

Testing Formsets

Testing Model Formsets

from django.forms import modelformset_factory, inlineformset_factory

class FormsetTests(TestCase):
    """Test Django formsets"""
    
    def setUp(self):
        self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
        self.category = Category.objects.create(name='Tech', slug='tech')
        
        self.post = BlogPost.objects.create(
            title='Test Post',
            content='Test content',
            author=self.user,
            category=self.category
        )
    
    def test_model_formset_creation(self):
        """Test creating model formset"""
        
        # Create formset class
        BlogPostFormSet = modelformset_factory(
            BlogPost,
            fields=['title', 'content', 'status'],
            extra=2  # 2 extra empty forms
        )
        
        # Create formset instance
        formset = BlogPostFormSet(queryset=BlogPost.objects.none())
        
        # Check formset properties
        self.assertEqual(len(formset.forms), 2)  # 2 extra forms
        self.assertTrue(formset.is_valid())  # Empty formset is valid
    
    def test_model_formset_with_data(self):
        """Test model formset with data"""
        
        BlogPostFormSet = modelformset_factory(
            BlogPost,
            fields=['title', 'content', 'status'],
            extra=1
        )
        
        # Formset data (Django expects specific naming)
        formset_data = {
            'form-TOTAL_FORMS': '2',
            'form-INITIAL_FORMS': '0',
            'form-MIN_NUM_FORMS': '0',
            'form-MAX_NUM_FORMS': '1000',
            
            # First form
            'form-0-title': 'First Post',
            'form-0-content': 'Content for first post',
            'form-0-status': 'draft',
            
            # Second form
            'form-1-title': 'Second Post',
            'form-1-content': 'Content for second post',
            'form-1-status': 'published',
        }
        
        formset = BlogPostFormSet(data=formset_data)
        
        self.assertTrue(formset.is_valid())
        
        # Save formset
        instances = formset.save(commit=False)
        for instance in instances:
            instance.author = self.user
            instance.category = self.category
            instance.save()
        
        # Verify posts were created
        self.assertEqual(BlogPost.objects.count(), 3)  # Original + 2 new
        self.assertTrue(BlogPost.objects.filter(title='First Post').exists())
        self.assertTrue(BlogPost.objects.filter(title='Second Post').exists())
    
    def test_inline_formset(self):
        """Test inline formset for related models"""
        
        # Assuming Comment model with ForeignKey to BlogPost
        from blog.models import Comment
        
        CommentFormSet = inlineformset_factory(
            BlogPost,
            Comment,
            fields=['content', 'author_name', 'author_email'],
            extra=2
        )
        
        formset_data = {
            'comment_set-TOTAL_FORMS': '2',
            'comment_set-INITIAL_FORMS': '0',
            'comment_set-MIN_NUM_FORMS': '0',
            'comment_set-MAX_NUM_FORMS': '1000',
            
            # First comment
            'comment_set-0-content': 'Great post!',
            'comment_set-0-author_name': 'John Doe',
            'comment_set-0-author_email': 'john@example.com',
            
            # Second comment
            'comment_set-1-content': 'Thanks for sharing!',
            'comment_set-1-author_name': 'Jane Smith',
            'comment_set-1-author_email': 'jane@example.com',
        }
        
        formset = CommentFormSet(data=formset_data, instance=self.post)
        
        if formset.is_valid():
            formset.save()
            
            # Verify comments were created
            self.assertEqual(self.post.comment_set.count(), 2)
            self.assertTrue(
                self.post.comment_set.filter(content='Great post!').exists()
            )
        else:
            # Debug formset errors
            for form in formset:
                if form.errors:
                    print(f"Form errors: {form.errors}")
            if formset.non_form_errors():
                print(f"Non-form errors: {formset.non_form_errors()}")
    
    def test_formset_validation_errors(self):
        """Test formset validation errors"""
        
        BlogPostFormSet = modelformset_factory(
            BlogPost,
            fields=['title', 'content'],
            extra=1
        )
        
        # Invalid data (missing required fields)
        formset_data = {
            'form-TOTAL_FORMS': '1',
            'form-INITIAL_FORMS': '0',
            'form-MIN_NUM_FORMS': '0',
            'form-MAX_NUM_FORMS': '1000',
            
            'form-0-title': '',  # Missing required field
            'form-0-content': 'Content without title',
        }
        
        formset = BlogPostFormSet(data=formset_data)
        
        self.assertFalse(formset.is_valid())
        
        # Check form errors
        self.assertTrue(formset.forms[0].errors)
        self.assertIn('title', formset.forms[0].errors)

Testing Form Security

Testing CSRF Protection

from django.test import TestCase, Client
from django.middleware.csrf import get_token

class FormSecurityTests(TestCase):
    """Test form security features"""
    
    def setUp(self):
        self.client = Client(enforce_csrf_checks=True)
        self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
        self.category = Category.objects.create(name='Tech', slug='tech')
    
    def test_csrf_protection_enabled(self):
        """Test CSRF protection is enabled"""
        
        self.client.login(username='testuser', password='testpass123')
        
        # POST without CSRF token should fail
        url = reverse('blog:post_create')
        data = {
            'title': 'Test Post',
            'content': 'Test content',
            'category': self.category.id
        }
        
        response = self.client.post(url, data)
        
        # Should be forbidden due to missing CSRF token
        self.assertEqual(response.status_code, 403)
    
    def test_csrf_token_in_form(self):
        """Test CSRF token is included in form"""
        
        self.client.force_login(self.user)
        
        url = reverse('blog:post_create')
        response = self.client.get(url)
        
        # Check CSRF token is in form
        self.assertContains(response, 'csrfmiddlewaretoken')
        self.assertContains(response, 'name="csrfmiddlewaretoken"')
    
    def test_valid_csrf_token_allows_submission(self):
        """Test valid CSRF token allows form submission"""
        
        # Get CSRF token
        self.client.force_login(self.user)
        url = reverse('blog:post_create')
        response = self.client.get(url)
        
        # Extract CSRF token from response
        csrf_token = get_token(response.wsgi_request)
        
        # Submit form with CSRF token
        data = {
            'title': 'Test Post',
            'content': 'Test content with more than fifty words. ' * 10,
            'category': self.category.id,
            'status': 'draft',
            'csrfmiddlewaretoken': csrf_token
        }
        
        response = self.client.post(url, data)
        
        # Should succeed (redirect after successful creation)
        self.assertEqual(response.status_code, 302)

Next Steps

With comprehensive form testing in place, you're ready to move on to testing templates. The next chapter will cover testing Django templates, including template rendering, context variables, template tags, and filters.

Key form testing concepts covered:

  • Basic form validation testing
  • Custom validation method testing
  • Form field and widget testing
  • ModelForm integration testing
  • Form inheritance testing
  • Formset testing
  • Form security testing

Form tests ensure your application correctly validates user input, maintains data integrity, and provides a secure interface for user interactions.