pytest-django Testing Patterns
TDD Workflow (RED-GREEN-REFACTOR)
Always follow this cycle:
-
RED: Write a failing test first that describes desired behavior
-
GREEN: Write minimal code to make the test pass
-
REFACTOR: Clean up code while keeping tests green
-
REPEAT: Never write production code without a failing test
Critical rule: If implementing a feature or fixing a bug, write the test BEFORE touching production code.
Essential pytest-django Patterns
Database Access
-
Use @pytest.mark.django_db on any test touching the database
-
Apply to entire module: pytestmark = pytest.mark.django_db
-
Transactions roll back automatically after each test
Fixtures for Test Data
Use Factory Boy for models, pytest fixtures for setup:
Factories: Create model instances with realistic data (UserFactory() )
-
Use factory.Sequence() for unique fields
-
Use factory.Faker() for realistic fake data
-
Use factory.SubFactory() for foreign keys
-
Use @factory.post_generation for M2M relationships
Fixtures: Setup clients, auth state, or shared resources
-
client fixture: Django test client
-
Create auth_client fixture: client.force_login(user) for authenticated requests
-
Define in conftest.py for reuse across test files
Test Organization
Structure tests to mirror app structure:
tests/ ├── apps/ │ └── posts/ │ ├── test_models.py │ ├── test_views.py │ └── test_forms.py ├── factories.py └── conftest.py
Group related tests in classes:
-
Name classes TestComponentName (e.g., TestPostListView )
-
Name test methods descriptively: test_<action>_<expected_outcome>
-
Use @pytest.mark.parametrize for testing multiple scenarios
What to Test
Views
-
Status codes: Correct HTTP responses (200, 404, 302)
-
Authentication: Authenticated vs anonymous behavior
-
Authorization: User can only access their own data
-
Context data: Correct objects passed to template
-
Side effects: Database changes, emails sent, tasks queued
-
HTMX: Check HTTP_HX_REQUEST header returns partial template
Forms
-
Validation: Valid data passes, invalid data fails with correct errors
-
Edge cases: Empty fields, max lengths, unique constraints
-
Clean methods: Custom validation logic works
-
Save behavior: Objects created/updated correctly
Models
-
Methods: str , custom methods return expected values
-
Managers/QuerySets: Custom filtering works correctly
-
Constraints: Database-level validation enforced
-
Signals: Pre/post save hooks execute correctly
Celery Tasks
-
Mock external calls: Patch HTTP requests, email sending, etc.
-
Test logic only: Don't test actual async execution
-
Idempotency: Running task multiple times is safe
Django-Specific Testing Patterns
Testing HTMX Responses
Check partial template rendered when HX-Request header present:
-
Pass HTTP_HX_REQUEST="true" to client request
-
Assert response.templates contains partial template name
Testing Permissions
Create authenticated vs anonymous client fixtures:
-
Test redirect/403 for unauthorized access
-
Test success for authorized access
Testing QuerySets
Verify efficient queries:
-
Create test data with factories
-
Execute query
-
Assert correct objects returned/excluded
-
Verify related objects loaded with select_related() /prefetch_related()
Testing Forms with Model Instances
Pass instance to form for updates:
-
form = MyForm(data=new_data, instance=existing_obj)
-
Verify form.save() updates, doesn't create
Common Patterns
Parametrize multiple scenarios: Use @pytest.mark.parametrize("input,expected", [...]) for testing various inputs
Mock external services: Use mocker.patch() to avoid actual HTTP calls, emails, file operations
Check database changes:
-
Assert Model.objects.filter(...).exists() after creation
-
Assert Model.objects.count() == expected for deletions
-
Use refresh_from_db() to verify updates
Test error handling:
-
Invalid form data produces correct errors
-
Failed operations return error responses
-
User sees appropriate error messages
Running Tests
uv run pytest # All tests uv run pytest -x # Stop on first failure uv run pytest --lf # Run last failed uv run pytest -x --lf # Stop first, last failed only uv run pytest -k "test_name" # Run tests matching pattern uv run pytest tests/apps/posts/ # Specific directory uv run pytest --cov=apps # With coverage report
Common Pitfalls
-
Forgetting @pytest.mark.django_db : Results in "Database access not allowed" errors
-
Not using factories: Creating instances manually is verbose and brittle
-
Testing implementation: Test behavior and outcomes, not internal implementation details
-
Skipping TDD: Writing tests after code means tests follow implementation, missing edge cases
-
Over-mocking: Mock external dependencies, not your own code
-
Testing framework code: Don't test Django's ORM, form validation, etc. Test YOUR logic
Setup Requirements
In pyproject.toml :
[tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings.test" python_files = ["test_*.py"] addopts = ["--reuse-db", "-ra"]
In conftest.py : Define shared fixtures (auth_client, common factories, etc.)
Integration with Other Skills
-
systematic-debugging: When fixing bugs, write failing test first to reproduce
-
django-models: Test custom managers, QuerySets, and model methods
-
django-forms: Test form validation, clean methods, and save behavior
-
celery-patterns: Test task logic with mocked external dependencies