django-tdd
SKILL.md
Django TDD Patterns
Test-driven development workflow for Django 5.x with pytest.
Setup
# pytest.ini or pyproject.toml
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings.test"
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "--reuse-db --tb=short -q"
markers = [
"slow: marks tests as slow",
"integration: marks integration tests",
]
# config/settings/test.py
from .base import *
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": ":memory:",
}
}
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] # Faster tests
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
DEFAULT_FILE_STORAGE = "django.core.files.storage.InMemoryStorage"
Factory Pattern with factory_boy
# apps/users/tests/factories.py
import factory
from apps.users.models import User, Profile
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
skip_postgeneration_save = True
name = factory.Faker("name")
email = factory.LazyAttribute(lambda o: f"{o.name.lower().replace(' ', '.')}@example.com")
is_active = True
@factory.post_generation
def with_profile(self, create, extracted, **kwargs):
if not create or not extracted:
return
ProfileFactory(user=self)
class ProfileFactory(factory.django.DjangoModelFactory):
class Meta:
model = Profile
user = factory.SubFactory(UserFactory)
bio = factory.Faker("paragraph")
TDD Cycle: Red-Green-Refactor
Step 1: Write Failing Test (Red)
# apps/users/tests/test_services.py
import pytest
from apps.users.services import UserService
from apps.users.models import User
@pytest.mark.django_db
class TestUserService:
def test_create_user_returns_user(self):
user = UserService.create_user(name="Alice", email="alice@example.com")
assert isinstance(user, User)
assert user.name == "Alice"
assert user.email == "alice@example.com"
def test_create_user_creates_profile(self):
user = UserService.create_user(name="Alice", email="alice@example.com")
assert hasattr(user, "profile")
assert user.profile is not None
def test_create_user_duplicate_email_raises(self):
UserService.create_user(name="Alice", email="alice@example.com")
with pytest.raises(Exception):
UserService.create_user(name="Bob", email="alice@example.com")
Step 2: Minimal Implementation (Green)
# apps/users/services.py
from django.db import transaction
from apps.users.models import User, Profile
class UserService:
@staticmethod
@transaction.atomic
def create_user(*, name: str, email: str) -> User:
user = User.objects.create(name=name, email=email)
Profile.objects.create(user=user)
return user
Step 3: Refactor (Keep Green)
Add error handling, logging, etc. while tests remain passing.
Model Testing
# apps/articles/tests/test_models.py
import pytest
from apps.articles.models import Article
from apps.users.tests.factories import UserFactory
@pytest.mark.django_db
class TestArticle:
def test_str_returns_title(self):
article = Article(title="Hello World")
assert str(article) == "Hello World"
def test_published_manager_excludes_drafts(self):
author = UserFactory()
Article.objects.create(title="Published", author=author, status="published")
Article.objects.create(title="Draft", author=author, status="draft")
published = Article.objects.published()
assert published.count() == 1
assert published.first().title == "Published"
def test_slug_auto_generated(self):
author = UserFactory()
article = Article.objects.create(title="My Article", author=author)
assert article.slug == "my-article"
API Testing with DRF
# apps/users/tests/test_api.py
import pytest
from rest_framework.test import APIClient
from rest_framework import status
from apps.users.tests.factories import UserFactory
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def authenticated_client(api_client):
user = UserFactory()
api_client.force_authenticate(user=user)
return api_client, user
@pytest.mark.django_db
class TestUserAPI:
def test_list_requires_auth(self, api_client):
response = api_client.get("/api/users/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_list_returns_users(self, authenticated_client):
client, user = authenticated_client
UserFactory.create_batch(3)
response = client.get("/api/users/")
assert response.status_code == status.HTTP_200_OK
assert len(response.data["results"]) == 4 # 3 + authenticated user
def test_create_user(self, authenticated_client):
client, _ = authenticated_client
payload = {"name": "New User", "email": "new@example.com"}
response = client.post("/api/users/", payload)
assert response.status_code == status.HTTP_201_CREATED
assert response.data["name"] == "New User"
def test_create_user_invalid_email(self, authenticated_client):
client, _ = authenticated_client
payload = {"name": "New User", "email": "not-an-email"}
response = client.post("/api/users/", payload)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "email" in response.data
def test_update_user(self, authenticated_client):
client, user = authenticated_client
response = client.patch(f"/api/users/{user.id}/", {"name": "Updated"})
assert response.status_code == status.HTTP_200_OK
user.refresh_from_db()
assert user.name == "Updated"
def test_delete_user(self, authenticated_client):
client, _ = authenticated_client
target = UserFactory()
response = client.delete(f"/api/users/{target.id}/")
assert response.status_code == status.HTTP_204_NO_CONTENT
View Testing
# apps/articles/tests/test_views.py
import pytest
from django.test import Client
from django.urls import reverse
from apps.users.tests.factories import UserFactory
@pytest.mark.django_db
class TestArticleViews:
def test_list_view_status(self):
client = Client()
response = client.get(reverse("article-list"))
assert response.status_code == 200
def test_list_view_template(self):
client = Client()
response = client.get(reverse("article-list"))
assert "articles/list.html" in [t.name for t in response.templates]
def test_create_view_requires_login(self):
client = Client()
response = client.get(reverse("article-create"))
assert response.status_code == 302 # Redirect to login
Fixtures and Conftest
# conftest.py (project root)
import pytest
from rest_framework.test import APIClient
from apps.users.tests.factories import UserFactory
@pytest.fixture
def user():
return UserFactory()
@pytest.fixture
def admin_user():
return UserFactory(is_staff=True, is_superuser=True)
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def auth_api_client(user):
client = APIClient()
client.force_authenticate(user=user)
return client
Testing Async Views
import pytest
from django.test import AsyncClient
@pytest.mark.django_db
@pytest.mark.asyncio
class TestAsyncViews:
async def test_async_endpoint(self):
client = AsyncClient()
response = await client.get("/api/async-data/")
assert response.status_code == 200
Coverage Configuration
# pyproject.toml
[tool.coverage.run]
source = ["apps"]
omit = ["*/migrations/*", "*/tests/*", "*/admin.py"]
[tool.coverage.report]
fail_under = 80
show_missing = true
Test Commands
# Run all tests
pytest
# Run with coverage
pytest --cov=apps --cov-report=term-missing
# Run specific app
pytest apps/users/
# Run marked tests
pytest -m "not slow"
# Run in parallel
pytest -n auto # requires pytest-xdist
Checklist
- Every service method has a corresponding test
- API endpoints tested for auth, valid input, invalid input, edge cases
- Factory patterns used instead of raw
Model.objects.create - Test settings use in-memory DB and fast password hasher
- Coverage threshold set to 80% minimum
- Tests use
@pytest.mark.django_dbfor database access - Fixtures defined in conftest.py for reuse
- Integration tests marked separately from unit tests
Weekly Installs
2
Repository
peopleforrester…dotfilesGitHub Stars
1
First Seen
14 days ago
Installed on
opencode2
gemini-cli2
codebuddy2
github-copilot2
codex2
kimi-cli2