django-tests
SKILL.md
Django Testing
You are a Django testing expert. Your goal is to help write fast, reliable, and maintainable tests.
Initial Assessment
Check for project context first:
If .agents/django-project-context.md exists, read it to understand the test runner (pytest-django vs Django's test runner), existing test structure, and any factory patterns already in use.
Setup: pytest-django (Recommended)
pip install pytest-django pytest-cov factory-boy faker
# pytest.ini or pyproject.toml
[pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
python_files = tests/*.py tests/**/*.py
python_classes = Test*
python_functions = test_*
addopts = --reuse-db -p no:warnings
# conftest.py (root)
import pytest
@pytest.fixture(autouse=True)
def reset_db(db):
"""Ensure database access is controlled per test."""
pass
Test Organization
app/
└── tests/
├── __init__.py
├── conftest.py # Fixtures for this app
├── factories.py # factory_boy factories
├── test_models.py # Model unit tests
├── test_views.py # View / template tests
├── test_api.py # API endpoint tests
└── test_services.py # Business logic tests
Factories with factory_boy
Always use factories instead of raw model creation in tests:
# tests/factories.py
import factory
from factory.django import DjangoModelFactory
from faker import Faker
from django.contrib.auth import get_user_model
from articles.models import Article, Tag
fake = Faker()
class UserFactory(DjangoModelFactory):
class Meta:
model = get_user_model()
username = factory.Sequence(lambda n: f'user{n}')
email = factory.LazyAttribute(lambda o: f'{o.username}@example.com')
password = factory.PostGenerationMethodCall('set_password', 'testpass123')
is_active = True
class TagFactory(DjangoModelFactory):
class Meta:
model = Tag
name = factory.Sequence(lambda n: f'tag-{n}')
class ArticleFactory(DjangoModelFactory):
class Meta:
model = Article
title = factory.LazyFunction(fake.sentence)
content = factory.LazyFunction(fake.text)
author = factory.SubFactory(UserFactory)
status = Article.Status.DRAFT
@factory.post_generation
def tags(self, create, extracted, **kwargs):
if not create:
return
if extracted:
for tag in extracted:
self.tags.add(tag)
Model Tests
# tests/test_models.py
import pytest
from .factories import ArticleFactory, UserFactory
@pytest.mark.django_db
class TestArticleModel:
def test_str_representation(self):
article = ArticleFactory(title='Hello World')
assert str(article) == 'Hello World'
def test_default_status_is_draft(self):
article = ArticleFactory()
assert article.status == Article.Status.DRAFT
def test_publish_sets_status(self):
article = ArticleFactory()
article.publish()
assert article.status == Article.Status.PUBLISHED
def test_slug_generated_on_save(self):
article = ArticleFactory(title='My Test Article', slug='')
assert article.slug == 'my-test-article'
View Tests
# tests/test_views.py
import pytest
from django.urls import reverse
from .factories import ArticleFactory, UserFactory
@pytest.mark.django_db
class TestArticleViews:
def test_list_requires_login(self, client):
url = reverse('articles:list')
response = client.get(url)
assert response.status_code == 302
assert '/login/' in response['Location']
def test_list_shows_user_articles(self, client):
user = UserFactory()
their_articles = ArticleFactory.create_batch(3, author=user)
other_article = ArticleFactory() # Belongs to another user
client.force_login(user)
response = client.get(reverse('articles:list'))
assert response.status_code == 200
assert len(response.context['articles']) == 3
def test_create_article(self, client):
user = UserFactory()
client.force_login(user)
response = client.post(reverse('articles:create'), {
'title': 'New Article',
'content': 'Article content here.',
'status': 'draft',
})
assert response.status_code == 302
assert Article.objects.filter(title='New Article', author=user).exists()
API Tests (DRF)
# tests/test_api.py
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
from .factories import ArticleFactory, UserFactory
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def auth_client(api_client):
user = UserFactory()
api_client.force_authenticate(user=user)
api_client.user = user
return api_client
@pytest.mark.django_db
class TestArticleAPI:
def test_list_articles(self, auth_client):
ArticleFactory.create_batch(3, author=auth_client.user)
response = auth_client.get('/api/articles/')
assert response.status_code == 200
assert response.data['count'] == 3
def test_create_article(self, auth_client):
payload = {'title': 'New Article', 'content': 'Content', 'status': 'draft'}
response = auth_client.post('/api/articles/', payload)
assert response.status_code == 201
assert response.data['title'] == 'New Article'
def test_unauthenticated_request_denied(self, api_client):
response = api_client.get('/api/articles/')
assert response.status_code == 401
def test_cannot_edit_others_article(self, auth_client):
other_article = ArticleFactory()
response = auth_client.patch(f'/api/articles/{other_article.pk}/', {'title': 'Hijacked'})
assert response.status_code in [403, 404]
Mocking
# tests/test_services.py
import pytest
from unittest.mock import patch, MagicMock
from articles.services import send_publication_email
@pytest.mark.django_db
class TestEmailService:
@patch('articles.services.send_mail')
def test_publication_email_sent(self, mock_send_mail):
article = ArticleFactory()
send_publication_email(article)
mock_send_mail.assert_called_once()
call_kwargs = mock_send_mail.call_args
assert article.title in call_kwargs[1]['subject']
@patch('articles.services.requests.get')
def test_external_api_call(self, mock_get):
mock_get.return_value = MagicMock(status_code=200, json=lambda: {'result': 'ok'})
# test your code...
Useful Fixtures
# conftest.py
import pytest
from .factories import UserFactory, ArticleFactory
@pytest.fixture
def user(db):
return UserFactory()
@pytest.fixture
def admin_user(db):
return UserFactory(is_staff=True, is_superuser=True)
@pytest.fixture
def published_article(db, user):
return ArticleFactory(author=user, status='published')
pytest Markers
@pytest.mark.django_db # Access DB (required for most Django tests)
@pytest.mark.django_db(transaction=True) # Real transactions (for Celery tests)
@pytest.mark.slow # Mark as slow (run with -m slow)
Coverage
# Run with coverage
pytest --cov=. --cov-report=html --cov-report=term-missing
# Exclude files from coverage
# .coveragerc
[run]
omit = */migrations/*, */tests/*, manage.py, conftest.py
For detailed test patterns: See references/test-patterns.md
Related Skills
- django-models: Understanding what to test in models
- django-views: Understanding view behavior to test
- django-drf: APIClient usage for REST API testing
- django-performance: Testing query counts with
assertNumQueries
Weekly Installs
3
Repository
ristemingov/dja…de-setupFirst Seen
6 days ago
Security Audits
Installed on
cline3
gemini-cli3
github-copilot3
codex3
kimi-cli3
cursor3