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
First Seen
6 days ago
Installed on
cline3
gemini-cli3
github-copilot3
codex3
kimi-cli3
cursor3