174 lines
6.0 KiB
Markdown
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
|