Django Testing Tools

Comprehensive techniques for testing Django web applications

Introduction to Django Testing

Django provides a robust testing framework built on top of Python's unittest library, but with additional features specifically designed for web applications. Testing Django applications is crucial for ensuring that your models, views, forms, and templates work correctly across the entire application stack.

mindmap root((Django Testing)) Test Types Unit Tests Integration Tests Functional Tests Test Components Models Views Forms Templates URLs Middleware Admin Testing Tools TestCase SimpleTestCase TransactionTestCase Client RequestFactory Utilities Fixtures Mocking Assertions

Real-world analogy: Think of Django testing as quality control for a car manufacturing plant. You test individual components (unit tests), how components interact (integration tests), and finally take the car for a test drive to ensure it performs as expected (functional tests).

Django Testing Setup

Project Structure

Django automatically creates a tests.py file in each app when you run startapp. For larger applications, it's often better to create a tests directory:

my_project/
├── manage.py
└── my_app/
    ├── __init__.py
    ├── models.py
    ├── views.py
    ├── forms.py
    └── tests/
        ├── __init__.py
        ├── test_models.py
        ├── test_views.py
        └── test_forms.py

Running Tests

Django tests can be run using the test command:

# Run all tests
python manage.py test

# Run tests for specific app
python manage.py test my_app

# Run specific test
python manage.py test my_app.tests.test_models.ModelTestCase

# Run tests with tags (Django 2.2+)
python manage.py test --tag=fast

Settings for Testing

Django automatically uses a separate test database and applies sensible defaults for testing:

You can customize test settings in your settings.py:

# settings.py

# Use a faster password hasher for testing
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.MD5PasswordHasher',
]

# Use in-memory SQLite for testing
if 'test' in sys.argv:
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': ':memory:',
        }
    }
    
# Custom test runner
TEST_RUNNER = 'my_app.test_runner.CustomTestRunner'

Django Test Case Classes

Django provides several test case classes for different testing needs:

graph TD A[unittest.TestCase] --> B[SimpleTestCase] B --> C[TransactionTestCase] C --> D[TestCase] D --> E[LiveServerTestCase]

SimpleTestCase

Used when you don't need database access:

from django.test import SimpleTestCase

class UtilityFunctionTests(SimpleTestCase):
    def test_normalize_email(self):
        result = normalize_email("User@EXAMPLE.com")
        self.assertEqual(result, "user@example.com")

TestCase

The most commonly used class, provides database access with automatic transaction rollback:

from django.test import TestCase
from myapp.models import Product

class ProductModelTests(TestCase):
    def setUp(self):
        # This data is saved to the test database
        self.product = Product.objects.create(
            name="Test Product",
            price=19.99,
            in_stock=True
        )
    
    def test_product_str(self):
        self.assertEqual(str(self.product), "Test Product")

TransactionTestCase

Used when you need to test transactions or database savepoints:

from django.test import TransactionTestCase
from django.db import transaction
from myapp.models import Account

class BankTransferTests(TransactionTestCase):
    def test_transfer_with_insufficient_funds(self):
        source = Account.objects.create(balance=50)
        destination = Account.objects.create(balance=0)
        
        # This will trigger a transaction rollback
        with self.assertRaises(ValueError):
            with transaction.atomic():
                source.transfer_to(destination, 100)

LiveServerTestCase

Launches a live Django server for testing with tools like Selenium:

from django.test import LiveServerTestCase
from selenium import webdriver

class MySeleniumTests(LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.selenium = webdriver.Chrome()
        cls.selenium.implicitly_wait(10)
    
    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()
    
    def test_login(self):
        self.selenium.get(f'{self.live_server_url}/accounts/login/')
        username_input = self.selenium.find_element_by_name("username")
        username_input.send_keys('myuser')
        # More test code here...

Real-world example: Instagram uses Django's TestCase extensively for their backend testing. They have found that creating specialized test case subclasses for different parts of their application helps organize tests and reduce test setup boilerplate.

Testing Django Models

Model testing typically focuses on:

from django.test import TestCase
from django.core.exceptions import ValidationError
from myapp.models import Product, Category

class ProductModelTests(TestCase):
    def setUp(self):
        self.category = Category.objects.create(name="Electronics")
        self.product = Product.objects.create(
            name="Smartphone",
            price=599.99,
            category=self.category,
            in_stock=True
        )
    
    def test_create_product(self):
        # Test object creation and retrieval
        product_count = Product.objects.count()
        self.assertEqual(product_count, 1)
        
        # Test field values
        self.assertEqual(self.product.name, "Smartphone")
        self.assertEqual(self.product.price, 599.99)
        self.assertTrue(self.product.in_stock)
        
    def test_product_str_representation(self):
        # Test __str__ method
        self.assertEqual(str(self.product), "Smartphone")
        
    def test_product_relationships(self):
        # Test foreign key relationship
        self.assertEqual(self.product.category, self.category)
        
        # Test reverse relationship
        self.assertIn(self.product, self.category.products.all())
        
    def test_price_validation(self):
        # Test constraints/validation
        invalid_product = Product(
            name="Invalid Product",
            price=-10.00,  # Negative price
            category=self.category
        )
        
        with self.assertRaises(ValidationError):
            invalid_product.full_clean()
            
    def test_custom_manager_method(self):
        # Test custom manager
        in_stock_products = Product.objects.in_stock()
        self.assertEqual(in_stock_products.count(), 1)
        
        self.product.in_stock = False
        self.product.save()
        
        in_stock_products = Product.objects.in_stock()
        self.assertEqual(in_stock_products.count(), 0)
        
    def test_discount_method(self):
        # Test instance method
        discounted_price = self.product.apply_discount(10)
        self.assertEqual(discounted_price, 539.99)

Testing Model Signals

from django.test import TestCase
from django.db.models.signals import post_save
from myapp.models import User, Profile
from myapp.signals import create_user_profile

class ProfileSignalTests(TestCase):
    def setUp(self):
        # Disconnect the signal temporarily to isolate tests
        post_save.disconnect(create_user_profile, sender=User)
    
    def tearDown(self):
        # Reconnect signal after test
        post_save.connect(create_user_profile, sender=User)
    
    def test_profile_creation_signal(self):
        # Connect the signal for this test
        post_save.connect(create_user_profile, sender=User)
        
        user = User.objects.create_user(username='testuser', password='password')
        
        # Check that a profile was created
        self.assertTrue(hasattr(user, 'profile'))
        self.assertIsNotNone(user.profile)
        self.assertEqual(user.profile.user, user)

Testing Django Views

Django provides the Client class for simulating HTTP requests:

from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from myapp.models import Product

class ProductViewTests(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(
            username='testuser', 
            email='user@example.com', 
            password='testpassword'
        )
        self.product = Product.objects.create(
            name="Test Product",
            price=19.99,
            description="Test description"
        )
        self.list_url = reverse('product-list')
        self.detail_url = reverse('product-detail', args=[self.product.id])
    
    def test_product_list_view(self):
        # Test unauthenticated access
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'myapp/product_list.html')
        self.assertContains(response, "Test Product")
        
        # Check context
        self.assertIn('products', response.context)
        self.assertEqual(len(response.context['products']), 1)
        
    def test_product_detail_view(self):
        response = self.client.get(self.detail_url)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'myapp/product_detail.html')
        self.assertContains(response, "Test Product")
        self.assertContains(response, "Test description")
        
    def test_product_create_view(self):
        # Anonymous users can't create products
        create_url = reverse('product-create')
        response = self.client.get(create_url)
        self.assertEqual(response.status_code, 302)  # Redirect to login
        
        # Login and try again
        self.client.login(username='testuser', password='testpassword')
        response = self.client.get(create_url)
        self.assertEqual(response.status_code, 200)
        
        # Test POST request
        product_data = {
            'name': 'New Product',
            'price': 29.99,
            'description': 'A new test product'
        }
        response = self.client.post(create_url, product_data)
        
        # Check redirect on success
        self.assertEqual(response.status_code, 302)
        
        # Check that product was created
        self.assertEqual(Product.objects.count(), 2)
        new_product = Product.objects.get(name='New Product')
        self.assertEqual(new_product.price, 29.99)

Testing Django REST Framework Views

from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from myapp.models import Product

class ProductAPITests(APITestCase):
    def setUp(self):
        self.product = Product.objects.create(
            name="Test Product",
            price=19.99
        )
        self.list_url = reverse('api:product-list')
        self.detail_url = reverse('api:product-detail', args=[self.product.id])
    
    def test_get_product_list(self):
        response = self.client.get(self.list_url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 1)
        
    def test_get_product_detail(self):
        response = self.client.get(self.detail_url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data['name'], 'Test Product')
        
    def test_create_product(self):
        data = {'name': 'New API Product', 'price': 39.99}
        response = self.client.post(self.list_url, data)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Product.objects.count(), 2)
        
    def test_update_product(self):
        data = {'name': 'Updated Product', 'price': 29.99}
        response = self.client.put(self.detail_url, data)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        
        # Refresh from database
        self.product.refresh_from_db()
        self.assertEqual(self.product.name, 'Updated Product')
        
    def test_delete_product(self):
        response = self.client.delete(self.detail_url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(Product.objects.count(), 0)

Real-world example: Sentry, which is built on Django, has extensive view tests that ensure proper access control and data handling. They mock authentication and focus on testing the business logic of their views rather than the rendering of templates.

Testing Django Forms

Form testing focuses on validation and cleaning:

from django.test import TestCase
from myapp.forms import ProductForm
from myapp.models import Category

class ProductFormTests(TestCase):
    def setUp(self):
        self.category = Category.objects.create(name="Electronics")
        self.form_data = {
            'name': 'Test Product',
            'price': 29.99,
            'category': self.category.id,
            'description': 'A test product'
        }
    
    def test_valid_form(self):
        form = ProductForm(data=self.form_data)
        self.assertTrue(form.is_valid())
        
    def test_empty_form(self):
        form = ProductForm(data={})
        self.assertFalse(form.is_valid())
        
        # Required fields should have errors
        self.assertIn('name', form.errors)
        self.assertIn('price', form.errors)
        self.assertIn('category', form.errors)
        
        # Description is optional
        self.assertNotIn('description', form.errors)
        
    def test_negative_price_validation(self):
        invalid_data = self.form_data.copy()
        invalid_data['price'] = -10.00
        
        form = ProductForm(data=invalid_data)
        self.assertFalse(form.is_valid())
        self.assertIn('price', form.errors)
        
    def test_name_max_length(self):
        invalid_data = self.form_data.copy()
        invalid_data['name'] = 'A' * 101  # Assuming max_length is 100
        
        form = ProductForm(data=invalid_data)
        self.assertFalse(form.is_valid())
        self.assertIn('name', form.errors)
        
    def test_custom_clean_method(self):
        # Assuming the form has custom clean method that validates
        # that premium products (price > 1000) must have a description
        invalid_data = self.form_data.copy()
        invalid_data['price'] = 1500.00
        invalid_data['description'] = ''
        
        form = ProductForm(data=invalid_data)
        self.assertFalse(form.is_valid())
        self.assertIn('__all__', form.errors)  # Non-field error
        
    def test_form_save(self):
        form = ProductForm(data=self.form_data)
        self.assertTrue(form.is_valid())
        
        product = form.save()
        self.assertEqual(product.name, 'Test Product')
        self.assertEqual(product.price, 29.99)
        self.assertEqual(product.category, self.category)

Testing ModelForms

from django.test import TestCase
from django.core.files.uploadedfile import SimpleUploadedFile
from myapp.forms import ProfileForm
from myapp.models import Profile

class ProfileFormTests(TestCase):
    def setUp(self):
        self.form_data = {
            'name': 'John Doe',
            'email': 'john@example.com',
            'bio': 'Test bio'
        }
        
        # For testing file uploads
        self.image = SimpleUploadedFile(
            name='test_image.jpg',
            content=b'file_content',
            content_type='image/jpeg'
        )
        
    def test_form_with_file_upload(self):
        form_data = self.form_data.copy()
        form_data['avatar'] = self.image
        
        form = ProfileForm(data=form_data, files={'avatar': self.image})
        self.assertTrue(form.is_valid())
        
        profile = form.save()
        self.assertEqual(profile.name, 'John Doe')
        self.assertTrue(profile.avatar.name.endswith('.jpg'))
        
    def test_email_uniqueness(self):
        # Create a profile with the same email
        Profile.objects.create(
            name='Existing User',
            email='john@example.com'
        )
        
        form = ProfileForm(data=self.form_data)
        self.assertFalse(form.is_valid())
        self.assertIn('email', form.errors)

Testing Django Templates and URLs

Template Testing

Templates are typically tested indirectly through view tests, but you can also test them directly:

from django.test import TestCase
from django.template import Context, Template

class TemplateTagTests(TestCase):
    def test_custom_template_tag(self):
        # Load template with {% load custom_tags %}
        template = Template(
            '{% load custom_tags %}'
            '{% format_currency 12.34 "USD" %}'
        )
        rendered = template.render(Context({}))
        self.assertEqual(rendered, '$12.34')
        
    def test_template_filter(self):
        template = Template(
            '{% load custom_filters %}'
            '{{ "hello world"|capitalize }}'
        )
        rendered = template.render(Context({}))
        self.assertEqual(rendered, 'Hello World')

URL Testing

from django.test import TestCase
from django.urls import reverse, resolve
from myapp.views import ProductListView, ProductDetailView

class UrlsTests(TestCase):
    def test_product_list_url(self):
        url = reverse('product-list')
        self.assertEqual(url, '/products/')
        
        # Test URL resolution
        resolver = resolve('/products/')
        self.assertEqual(resolver.func.__name__, ProductListView.as_view().__name__)
        
    def test_product_detail_url(self):
        url = reverse('product-detail', args=[1])
        self.assertEqual(url, '/products/1/')
        
        resolver = resolve('/products/1/')
        self.assertEqual(resolver.func.__name__, ProductDetailView.as_view().__name__)
        
        # Check kwargs
        self.assertEqual(resolver.kwargs, {'pk': '1'})
        
    def test_named_urls_in_templates(self):
        response = self.client.get(reverse('home'))
        self.assertContains(response, reverse('product-list'))

Testing Django Middleware and Authentication

Middleware Testing

from django.test import TestCase, RequestFactory
from django.http import HttpResponse
from myapp.middleware import CustomMiddleware

class MiddlewareTests(TestCase):
    def setUp(self):
        self.factory = RequestFactory()
        
        # A simple view for testing middleware
        def test_view(request):
            return HttpResponse("Test view")
            
        self.test_view = test_view
        self.middleware = CustomMiddleware(self.test_view)
        
    def test_middleware_modifies_request(self):
        request = self.factory.get('/test/')
        response = self.middleware(request)
        
        # Check that middleware modified request
        self.assertTrue(hasattr(request, 'middleware_was_here'))
        
    def test_middleware_modifies_response(self):
        request = self.factory.get('/test/')
        response = self.middleware(request)
        
        # Check that middleware modified response
        self.assertEqual(response['X-Middleware-Header'], 'Value')

Authentication Testing

from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User

class AuthenticationTests(TestCase):
    def setUp(self):
        self.client = Client()
        self.login_url = reverse('login')
        self.protected_url = reverse('profile')
        
        # Create test user
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpassword'
        )
        
    def test_login_view(self):
        # GET request should show form
        response = self.client.get(self.login_url)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'login.html')
        
        # Incorrect login
        response = self.client.post(self.login_url, {
            'username': 'testuser',
            'password': 'wrongpassword'
        })
        self.assertEqual(response.status_code, 200)  # Stay on login page
        self.assertFalse(response.context['user'].is_authenticated)
        
        # Correct login
        response = self.client.post(self.login_url, {
            'username': 'testuser',
            'password': 'testpassword'
        }, follow=True)  # follow=True follows redirects
        
        self.assertTrue(response.context['user'].is_authenticated)
        self.assertRedirects(response, reverse('home'))
        
    def test_protected_view(self):
        # Unauthenticated access
        response = self.client.get(self.protected_url)
        self.assertEqual(response.status_code, 302)  # Redirect to login
        
        # Login
        self.client.login(username='testuser', password='testpassword')
        
        # Access protected view
        response = self.client.get(self.protected_url)
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'profile.html')
        
    def test_user_permissions(self):
        # Create admin user
        admin = User.objects.create_superuser(
            username='admin',
            email='admin@example.com',
            password='adminpassword'
        )
        
        # Regular user can't access admin
        self.client.login(username='testuser', password='testpassword')
        response = self.client.get(reverse('admin:index'))
        self.assertEqual(response.status_code, 302)  # Redirect
        
        # Admin can access admin
        self.client.login(username='admin', password='adminpassword')
        response = self.client.get(reverse('admin:index'))
        self.assertEqual(response.status_code, 200)

Real-world example: Pinterest, which uses Django, has a thorough authentication test suite that verifies their multi-factor authentication flows, social logins, and permission checks.

Using Test Fixtures in Django

Django provides multiple ways to set up test data:

Using setUp method

def setUp(self):
    self.user = User.objects.create_user(...)
    self.product = Product.objects.create(...)

Using fixtures files

# fixtures/categories.json
[
  {
    "model": "myapp.category",
    "pk": 1,
    "fields": {
      "name": "Electronics",
      "description": "Electronic devices"
    }
  },
  {
    "model": "myapp.category",
    "pk": 2,
    "fields": {
      "name": "Books",
      "description": "Paper and digital books"
    }
  }
]

# In your test class
class ProductTests(TestCase):
    fixtures = ['categories.json', 'products.json']
    
    def test_something(self):
        # Fixtures are loaded, you can use them
        category = Category.objects.get(name="Electronics")
        self.assertEqual(category.id, 1)

Using factory_boy

factory_boy is a popular third-party library for creating test data:

# factories.py
import factory
from django.contrib.auth.models import User
from myapp.models import Category, Product

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User
        
    username = factory.Sequence(lambda n: f'user{n}')
    email = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')
    is_active = True
    
    @factory.post_generation
    def set_password(self, create, extracted, **kwargs):
        self.set_password('password')
        self.save()
        
class CategoryFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Category
        
    name = factory.Sequence(lambda n: f'Category {n}')
    description = factory.Faker('sentence')
    
class ProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Product
        
    name = factory.Sequence(lambda n: f'Product {n}')
    price = factory.Faker('pydecimal', left_digits=3, right_digits=2, positive=True)
    category = factory.SubFactory(CategoryFactory)
    description = factory.Faker('paragraph')
    in_stock = True

# In your tests
from myapp.factories import UserFactory, ProductFactory

class ProductTests(TestCase):
    def setUp(self):
        self.user = UserFactory()
        self.products = ProductFactory.create_batch(10)
        
    def test_product_listing(self):
        # 10 products have been created
        self.client.login(username=self.user.username, password='password')
        response = self.client.get(reverse('product-list'))
        self.assertEqual(len(response.context['products']), 10)

Mocking in Django Tests

The unittest.mock module is useful for isolating tests from external dependencies:

from django.test import TestCase
from unittest.mock import patch, MagicMock
from myapp.services import PaymentService
from myapp.views import make_payment

class PaymentTests(TestCase):
    @patch('myapp.services.PaymentService.process_payment')
    def test_payment_success(self, mock_process):
        # Configure the mock
        mock_process.return_value = {
            'success': True,
            'transaction_id': '12345'
        }
        
        # Make the request
        response = self.client.post(reverse('make-payment'), {
            'amount': '100.00',
            'card_number': '4111111111111111',
            'expiry': '12/25',
            'cvv': '123'
        })
        
        # Check that the mock was called with correct args
        mock_process.assert_called_once()
        call_args = mock_process.call_args[0]
        self.assertEqual(call_args[0], '100.00')
        
        # Check response
        self.assertEqual(response.status_code, 302)  # Redirect on success
        self.assertIn('success', response.url)
        
    @patch('myapp.services.PaymentService.process_payment')
    def test_payment_failure(self, mock_process):
        # Configure the mock to simulate failure
        mock_process.return_value = {
            'success': False,
            'error': 'Insufficient funds'
        }
        
        # Make the request
        response = self.client.post(reverse('make-payment'), {
            'amount': '100.00',
            'card_number': '4111111111111111',
            'expiry': '12/25',
            'cvv': '123'
        })
        
        # Check response
        self.assertEqual(response.status_code, 200)  # Stay on page
        self.assertContains(response, 'Insufficient funds')
        
    @patch('requests.post')
    def test_external_api_call(self, mock_post):
        # Mock the external API call
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {'status': 'success'}
        mock_post.return_value = mock_response
        
        service = PaymentService()
        result = service.process_payment('100.00', '4111111111111111', '12/25', '123')
        
        # Check the API was called correctly
        mock_post.assert_called_once_with(
            'https://payment.example.com/api/v1/payments',
            json={
                'amount': '100.00',
                'card_number': '4111111111111111',
                'expiry': '12/25',
                'cvv': '123'
            },
            headers={'Authorization': 'Bearer API_KEY'}
        )
        
        # Check result
        self.assertTrue(result['success'])

Testing Django Admin

from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from myapp.models import Product
from myapp.admin import ProductAdmin

class AdminTests(TestCase):
    def setUp(self):
        # Create superuser
        self.admin_user = User.objects.create_superuser(
            username='admin',
            email='admin@example.com',
            password='password'
        )
        self.client.login(username='admin', password='password')
        
        # Create test data
        self.product = Product.objects.create(
            name="Test Product",
            price=19.99,
            description="Test description"
        )
        
        # URLs
        self.admin_url = reverse('admin:index')
        self.product_list_url = reverse('admin:myapp_product_changelist')
        self.product_add_url = reverse('admin:myapp_product_add')
        self.product_change_url = reverse(
            'admin:myapp_product_change', args=[self.product.id]
        )
        
    def test_admin_accessible(self):
        response = self.client.get(self.admin_url)
        self.assertEqual(response.status_code, 200)
        
    def test_product_listed(self):
        response = self.client.get(self.product_list_url)
        self.assertContains(response, "Test Product")
        
    def test_product_edit(self):
        response = self.client.get(self.product_change_url)
        self.assertEqual(response.status_code, 200)
        
        # Update product
        response = self.client.post(self.product_change_url, {
            'name': 'Updated Product',
            'price': '29.99',
            'description': 'Updated description'
        })
        
        # Should redirect after successful edit
        self.assertEqual(response.status_code, 302)
        
        # Check product was updated
        self.product.refresh_from_db()
        self.assertEqual(self.product.name, 'Updated Product')
        
    def test_custom_admin_action(self):
        # Test a custom admin action
        response = self.client.post(self.product_list_url, {
            'action': 'mark_as_featured',
            '_selected_action': [self.product.id]
        })
        
        # Check product was updated
        self.product.refresh_from_db()
        self.assertTrue(self.product.is_featured)

Integration with PyTest

While Django's built-in test framework is powerful, many developers prefer to use PyTest for its additional features:

# Install pytest-django
pip install pytest-django

# pytest.ini or pyproject.toml
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = test_*.py *_test.py
django_find_project = true

Using PyTest Fixtures with Django

# conftest.py
import pytest
from django.contrib.auth.models import User
from myapp.models import Product, Category

@pytest.fixture
def user():
    return User.objects.create_user(
        username='testuser',
        email='test@example.com',
        password='password'
    )

@pytest.fixture
def admin_user():
    return User.objects.create_superuser(
        username='admin',
        email='admin@example.com',
        password='password'
    )

@pytest.fixture
def category():
    return Category.objects.create(name="Electronics")

@pytest.fixture
def product(category):
    return Product.objects.create(
        name="Test Product",
        price=19.99,
        category=category,
        description="Test description"
    )

# test_views.py
import pytest
from django.urls import reverse

@pytest.mark.django_db
def test_product_detail(client, product):
    url = reverse('product-detail', args=[product.id])
    response = client.get(url)
    
    assert response.status_code == 200
    assert "Test Product" in str(response.content)
    
@pytest.mark.django_db
def test_login_required(client, admin_client, product):
    url = reverse('product-edit', args=[product.id])
    
    # Anonymous user
    response = client.get(url)
    assert response.status_code == 302  # Redirect to login
    
    # Admin user
    response = admin_client.get(url)
    assert response.status_code == 200

Real-world example: Mozilla's Django projects use pytest-django for its better integration with their CI/CD pipelines and for the ability to use parameterized tests more easily than with Django's built-in test framework.

Testing for Performance and Security

Performance Testing

from django.test import TestCase
from django.test.utils import CaptureQueriesContext
from django.db import connection
from django.urls import reverse

class PerformanceTests(TestCase):
    def setUp(self):
        # Create test data
        self.categories = [Category.objects.create(name=f"Category {i}") for i in range(5)]
        
        for i in range(20):
            Product.objects.create(
                name=f"Product {i}",
                price=10 + i,
                category=self.categories[i % 5]
            )
            
    def test_query_count(self):
        with CaptureQueriesContext(connection) as context:
            response = self.client.get(reverse('product-list'))
            
        # Check that view doesn't execute too many queries
        self.assertLess(len(context), 5)
        
    def test_prefetch_related(self):
        with CaptureQueriesContext(connection) as context:
            # This view should prefetch related categories
            response = self.client.get(reverse('product-list-optimized'))
            
        # Should use fewer queries than non-optimized view
        query_count = len(context)
        
        with CaptureQueriesContext(connection) as context:
            # This view does not prefetch
            response = self.client.get(reverse('product-list-unoptimized'))
            
        # Non-optimized view should use more queries
        self.assertGreater(len(context), query_count)

Security Testing

from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User

class SecurityTests(TestCase):
    def setUp(self):
        self.regular_user = User.objects.create_user(
            username='user',
            password='password'
        )
        self.other_user = User.objects.create_user(
            username='otheruser',
            password='password'
        )
        self.admin_user = User.objects.create_superuser(
            username='admin',
            password='password'
        )
        
    def test_csrf_protection(self):
        # Login
        self.client.login(username='user', password='password')
        
        # Try to post without CSRF token
        self.client.handler.enforce_csrf_checks = True
        response = self.client.post(reverse('change-password'), {
            'new_password1': 'newpassword',
            'new_password2': 'newpassword'
        })
        
        # Should be forbidden
        self.assertEqual(response.status_code, 403)
        
    def test_authorization(self):
        # Regular user can't access other user's profile
        self.client.login(username='user', password='password')
        
        response = self.client.get(
            reverse('user-profile', args=[self.other_user.id])
        )
        self.assertEqual(response.status_code, 403)
        
        # Admin can access any profile
        self.client.login(username='admin', password='password')
        
        response = self.client.get(
            reverse('user-profile', args=[self.other_user.id])
        )
        self.assertEqual(response.status_code, 200)
        
    def test_xss_prevention(self):
        # Create product with potential XSS content
        product = Product.objects.create(
            name='',
            price=19.99
        )
        
        response = self.client.get(reverse('product-detail', args=[product.id]))
        
        # The script tag should be escaped in the response
        self.assertNotIn('', str(response.content))
        self.assertIn('<script>alert("XSS")</script>', str(response.content))

Practical Exercise

Exercise: Testing a Blog Application

Let's practice by writing tests for a simple blog application:

# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    published = models.BooleanField(default=False)
    
    def __str__(self):
        return self.title
        
    def get_absolute_url(self):
        return reverse('post-detail', args=[self.id])
        
class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return f"Comment by {self.author.username} on {self.post.title}"
# blog/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse_lazy
from .models import Post, Comment
from .forms import PostForm, CommentForm

class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    
    def get_queryset(self):
        queryset = super().get_queryset()
        if self.request.user.is_staff:
            return queryset
        return queryset.filter(published=True)
        
class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['comment_form'] = CommentForm()
        return context
        
class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
        
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'blog/post_form.html'
    
    def test_func(self):
        post = self.get_object()
        return self.request.user == post.author
        
class CommentCreateView(LoginRequiredMixin, CreateView):
    model = Comment
    form_class = CommentForm
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        form.instance.post_id = self.kwargs['pk']
        return super().form_valid(form)
        
    def get_success_url(self):
        return reverse_lazy('post-detail', kwargs={'pk': self.kwargs['pk']})

Your task:

  1. Write tests for the Post and Comment models
  2. Test the PostListView to ensure it filters unpublished posts correctly
  3. Test the PostDetailView and its context
  4. Test the CreateView and UpdateView with permission checks
  5. Test the CommentCreateView

Summary

Remember: Thorough testing of Django applications helps prevent regressions, ensures security, and provides documentation of expected behavior.

Assignment

Create a complete test suite for a Django e-commerce application with the following features:

  1. Build a simplified e-commerce application with these models:
    • Product (name, price, description, category, image)
    • Category (name, description)
    • Order (user, items, status, created_at)
    • OrderItem (order, product, quantity, price)
    • UserProfile (user, address, phone)
  2. Implement these views:
    • Product list and detail views
    • Shopping cart
    • Checkout process
    • Order history
    • User profile management
  3. Write a comprehensive test suite that:
    • Tests all models, views, and forms
    • Verifies authentication and permissions
    • Tests the checkout workflow
    • Checks for query optimization
    • Implements both unit and integration tests
  4. Use fixtures or factory_boy for test data
  5. Implement both Django's TestCase and pytest-django tests
  6. Test the admin interface customizations

Bonus challenge: Implement and test a REST API for the application using Django REST Framework.