django-tests

When the user wants to write tests for a Django application. Use when the user says "write tests," "unit test," "integration test," "pytest-django," "factory_boy," "test fixtures," "test views," "test models," "test API," "APIClient," "TestCase," "mock," "coverage," "conftest," or "test database." For API testing specifically with DRF, this skill covers both.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "django-tests" with this command: npx skills add ristemingov/django-claude-setup/ristemingov-django-claude-setup-django-tests

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

django-admin

No summary provided by upstream source.

Repository SourceNeeds Review
General

django-auth

No summary provided by upstream source.

Repository SourceNeeds Review
General

django-models

No summary provided by upstream source.

Repository SourceNeeds Review
General

django-views

No summary provided by upstream source.

Repository SourceNeeds Review