2026-04-15 17:11:42 -05:00

174 lines
6.0 KiB
Markdown

---
name: pytest-django-patterns
description: pytest-django testing patterns, Factory Boy, fixtures, and TDD workflow. Use when writing tests, creating test factories, or following TDD red-green-refactor cycle.
---
# pytest-django Testing Patterns
## TDD Workflow (RED-GREEN-REFACTOR)
**Always follow this cycle:**
1. **RED**: Write a failing test first that describes desired behavior
2. **GREEN**: Write minimal code to make the test pass
3. **REFACTOR**: Clean up code while keeping tests green
4. **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
```bash
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`:**
```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