MITupdated 2mo ago
pytestãfactoryboyãDjango REST Frameworkã䜿çšããDjangoã¢ããªã±ãŒã·ã§ã³ã®ãã¹ãé§åéçºã
What can you do with Django Tdd?
name: django-tdd description: Django testing strategies with pytest-django, TDD methodology, factory_boy, mocking, coverage, and testing Django REST Framework APIs.
Django ãã¹ãé§åéçº(TDD)
pytestãfactory_boyãDjango REST Frameworkã䜿çšããDjangoã¢ããªã±ãŒã·ã§ã³ã®ãã¹ãé§åéçºã
ãã€æå¹åããã
- æ°ããDjangoã¢ããªã±ãŒã·ã§ã³ãæžããšã
- Django REST Framework APIãå®è£ ãããšã
- Djangoã¢ãã«ããã¥ãŒãã·ãªã¢ã©ã€ã¶ãŒããã¹ããããšã
- Djangoãããžã§ã¯ãã®ãã¹ãã€ã³ãã©ãèšå®ãããšã
Djangoã®ããã®TDDã¯ãŒã¯ãããŒ
Red-Green-Refactorãµã€ã¯ã«
# ã¹ããã1: RED - 倱æãããã¹ããæžã
def test_user_creation():
user = User.objects.create_user(email='test@example.com', password='testpass123')
assert user.email == 'test@example.com'
assert user.check_password('testpass123')
assert not user.is_staff
# ã¹ããã2: GREEN - ãã¹ããéã
# Userã¢ãã«ãŸãã¯ãã¡ã¯ããªãŒãäœæ
# ã¹ããã3: REFACTOR - ãã¹ããã°ãªãŒã³ã«ä¿ã¡ãªããæ¹å
ã»ããã¢ãã
pytestèšå®
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--reuse-db
--nomigrations
--cov=apps
--cov-report=html
--cov-report=term-missing
--strict-markers
markers =
slow: marks tests as slow
integration: marks tests as integration tests
ãã¹ãèšå®
# config/settings/test.py
from .base import *
DEBUG = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
# ãã€ã°ã¬ãŒã·ã§ã³ãç¡å¹åããŠé«éå
class DisableMigrations:
def __contains__(self, item):
return True
def __getitem__(self, item):
return None
MIGRATION_MODULES = DisableMigrations()
# ããé«éãªãã¹ã¯ãŒãããã·ã³ã°
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
# ã¡ãŒã«ããã¯ãšã³ã
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Celeryã¯åžžã«eager
CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = True
conftest.py
# tests/conftest.py
import pytest
from django.utils import timezone
from django.contrib.auth import get_user_model
User = get_user_model()
@pytest.fixture(autouse=True)
def timezone_settings(settings):
"""äžè²«ããã¿ã€ã ãŸãŒã³ã確ä¿ã"""
settings.TIME_ZONE = 'UTC'
@pytest.fixture
def user(db):
"""ãã¹ããŠãŒã¶ãŒãäœæã"""
return User.objects.create_user(
email='test@example.com',
password='testpass123',
username='testuser'
)
@pytest.fixture
def admin_user(db):
"""管çè
ãŠãŒã¶ãŒãäœæã"""
return User.objects.create_superuser(
email='admin@example.com',
password='adminpass123',
username='admin'
)
@pytest.fixture
def authenticated_client(client, user):
"""èªèšŒæžã¿ã¯ã©ã€ã¢ã³ããè¿ãã"""
client.force_login(user)
return client
@pytest.fixture
def api_client():
"""DRF APIã¯ã©ã€ã¢ã³ããè¿ãã"""
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture
def authenticated_api_client(api_client, user):
"""èªèšŒæžã¿APIã¯ã©ã€ã¢ã³ããè¿ãã"""
api_client.force_authenticate(user=user)
return api_client
Factory Boy
ãã¡ã¯ããªãŒã»ããã¢ãã
# tests/factories.py
import factory
from factory import fuzzy
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from apps.products.models import Product, Category
User = get_user_model()
class UserFactory(factory.django.DjangoModelFactory):
"""Userã¢ãã«ã®ãã¡ã¯ããªãŒã"""
class Meta:
model = User
email = factory.Sequence(lambda n: f"user{n}@example.com")
username = factory.Sequence(lambda n: f"user{n}")
password = factory.PostGenerationMethodCall('set_password', 'testpass123')
first_name = factory.Faker('first_name')
last_name = factory.Faker('last_name')
is_active = True
class CategoryFactory(factory.django.DjangoModelFactory):
"""Categoryã¢ãã«ã®ãã¡ã¯ããªãŒã"""
class Meta:
model = Category
name = factory.Faker('word')
slug = factory.LazyAttribute(lambda obj: obj.name.lower())
description = factory.Faker('text')
class ProductFactory(factory.django.DjangoModelFactory):
"""Productã¢ãã«ã®ãã¡ã¯ããªãŒã"""
class Meta:
model = Product
name = factory.Faker('sentence', nb_words=3)
slug = factory.LazyAttribute(lambda obj: obj.name.lower().replace(' ', '-'))
description = factory.Faker('text')
price = fuzzy.FuzzyDecimal(10.00, 1000.00, 2)
stock = fuzzy.FuzzyInteger(0, 100)
is_active = True
category = factory.SubFactory(CategoryFactory)
created_by = factory.SubFactory(UserFactory)
@factory.post_generation
def tags(self, create, extracted, **kwargs):
"""補åã«ã¿ã°ã远å ã"""
if not create:
return
if extracted:
for tag in extracted:
self.tags.add(tag)
ãã¡ã¯ããªãŒã®äœ¿çš
# tests/test_models.py
import pytest
from tests.factories import ProductFactory, UserFactory
def test_product_creation():
"""ãã¡ã¯ããªãŒã䜿çšãã補åäœæããã¹ãã"""
product = ProductFactory(price=100.00, stock=50)
assert product.price == 100.00
assert product.stock == 50
assert product.is_active is True
def test_product_with_tags():
"""ã¿ã°ä»ã補åããã¹ãã"""
tags = [TagFactory(name='electronics'), TagFactory(name='new')]
product = ProductFactory(tags=tags)
assert product.tags.count() == 2
def test_multiple_products():
"""è€æ°ã®è£œåäœæããã¹ãã"""
products = ProductFactory.create_batch(10)
assert len(products) == 10
ã¢ãã«ãã¹ã
ã¢ãã«ãã¹ã
# tests/test_models.py
import pytest
from django.core.exceptions import ValidationError
from tests.factories import UserFactory, ProductFactory
class TestUserModel:
"""Userã¢ãã«ããã¹ãã"""
def test_create_user(self, db):
"""éåžžã®ãŠãŒã¶ãŒäœæããã¹ãã"""
user = UserFactory(email='test@example.com')
assert user.email == 'test@example.com'
assert user.check_password('testpass123')
assert not user.is_staff
assert not user.is_superuser
def test_create_superuser(self, db):
"""ã¹ãŒããŒãŠãŒã¶ãŒäœæããã¹ãã"""
user = UserFactory(
email='admin@example.com',
is_staff=True,
is_superuser=True
)
assert user.is_staff
assert user.is_superuser
def test_user_str(self, db):
"""ãŠãŒã¶ãŒã®æåå衚çŸããã¹ãã"""
user = UserFactory(email='test@example.com')
assert str(user) == 'test@example.com'
class TestProductModel:
"""Productã¢ãã«ããã¹ãã"""
def test_product_creation(self, db):
"""補åäœæããã¹ãã"""
product = ProductFactory()
assert product.id is not None
assert product.is_active is True
assert product.created_at is not None
def test_product_slug_generation(self, db):
"""èªåã¹ã©ãã°çæããã¹ãã"""
product = ProductFactory(name='Test Product')
assert product.slug == 'test-product'
def test_product_price_validation(self, db):
"""äŸ¡æ Œãè² ã®å€ã«ãªããªãããšããã¹ãã"""
product = ProductFactory(price=-10)
with pytest.raises(ValidationError):
product.full_clean()
def test_product_manager_active(self, db):
"""ã¢ã¯ãã£ããããŒãžã£ãŒã¡ãœããããã¹ãã"""
ProductFactory.create_batch(5, is_active=True)
ProductFactory.create_batch(3, is_active=False)
active_count = Product.objects.active().count()
assert active_count == 5
def test_product_stock_management(self, db):
"""åšåº«ç®¡çããã¹ãã"""
product = ProductFactory(stock=10)
product.reduce_stock(5)
product.refresh_from_db()
assert product.stock == 5
with pytest.raises(ValueError):
product.reduce_stock(10) # åšåº«äžè¶³
ãã¥ãŒãã¹ã
Djangoãã¥ãŒãã¹ã
# tests/test_views.py
import pytest
from django.urls import reverse
from tests.factories import ProductFactory, UserFactory
class TestProductViews:
"""補åãã¥ãŒããã¹ãã"""
def test_product_list(self, client, db):
"""補åãªã¹ããã¥ãŒããã¹ãã"""
ProductFactory.create_batch(10)
response = client.get(reverse('products:list'))
assert response.status_code == 200
assert len(response.context['products']) == 10
def test_product_detail(self, client, db):
"""補å詳现ãã¥ãŒããã¹ãã"""
product = ProductFactory()
response = client.get(reverse('products:detail', kwargs={'slug': product.slug}))
assert response.status_code == 200
assert response.context['product'] == product
def test_product_create_requires_login(self, client, db):
"""補åäœæã«èªèšŒãå¿
èŠã§ããããšããã¹ãã"""
response = client.get(reverse('products:create'))
assert response.status_code == 302
assert response.url.startswith('/accounts/login/')
def test_product_create_authenticated(self, authenticated_client, db):
"""èªèšŒæžã¿ãŠãŒã¶ãŒãšããŠã®è£œåäœæããã¹ãã"""
response = authenticated_client.get(reverse('products:create'))
assert response.status_code == 200
def test_product_create_post(self, authenticated_client, db, category):
"""POSTã«ãã補åäœæããã¹ãã"""
data = {
'name': 'Test Product',
'description': 'A test product',
'price': '99.99',
'stock': 10,
'category': category.id,
}
response = authenticated_client.post(reverse('products:create'), data)
assert response.status_code == 302
assert Product.objects.filter(name='Test Product').exists()
DRF APIãã¹ã
ã·ãªã¢ã©ã€ã¶ãŒãã¹ã
# tests/test_serializers.py
import pytest
from rest_framework.exceptions import ValidationError
from apps.products.serializers import ProductSerializer
from tests.factories import ProductFactory
class TestProductSerializer:
"""ProductSerializerããã¹ãã"""
def test_serialize_product(self, db):
"""補åã®ã·ãªã¢ã©ã€ãºããã¹ãã"""
product = ProductFactory()
serializer = ProductSerializer(product)
data = serializer.data
assert data['id'] == product.id
assert data['name'] == product.name
assert data['price'] == str(product.price)
def test_deserialize_product(self, db):
"""補åããŒã¿ã®ãã·ãªã¢ã©ã€ãºããã¹ãã"""
data = {
'name': 'Test Product',
'description': 'Test description',
'price': '99.99',
'stock': 10,
'category': 1,
}
serializer = ProductSerializer(data=data)
assert serializer.is_valid()
product = serializer.save()
assert product.name == 'Test Product'
assert float(product.price) == 99.99
def test_price_validation(self, db):
"""äŸ¡æ Œæ€èšŒããã¹ãã"""
data = {
'name': 'Test Product',
'price': '-10.00',
'stock': 10,
}
serializer = ProductSerializer(data=data)
assert not serializer.is_valid()
assert 'price' in serializer.errors
def test_stock_validation(self, db):
"""åšåº«ãè² ã«ãªããªãããšããã¹ãã"""
data = {
'name': 'Test Product',
'price': '99.99',
'stock': -5,
}
serializer = ProductSerializer(data=data)
assert not serializer.is_valid()
assert 'stock' in serializer.errors
API ViewSetãã¹ã
# tests/test_api.py
import pytest
from rest_framework.test import APIClient
from rest_framework import status
from django.urls import reverse
from tests.factories import ProductFactory, UserFactory
class TestProductAPI:
"""Product APIãšã³ããã€ã³ãããã¹ãã"""
@pytest.fixture
def api_client(self):
"""APIã¯ã©ã€ã¢ã³ããè¿ãã"""
return APIClient()
def test_list_products(self, api_client, db):
"""補åãªã¹ãããã¹ãã"""
ProductFactory.create_batch(10)
url = reverse('api:product-list')
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 10
def test_retrieve_product(self, api_client, db):
"""補åååŸããã¹ãã"""
product = ProductFactory()
url = reverse('api:product-detail', kwargs={'pk': product.id})
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.data['id'] == product.id
def test_create_product_unauthorized(self, api_client, db):
"""èªèšŒãªãã®è£œåäœæããã¹ãã"""
url = reverse('api:product-list')
data = {'name': 'Test Product', 'price': '99.99'}
response = api_client.post(url, data)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_create_product_authorized(self, authenticated_api_client, db):
"""èªèšŒæžã¿ãŠãŒã¶ãŒãšããŠã®è£œåäœæããã¹ãã"""
url = reverse('api:product-list')
data = {
'name': 'Test Product',
'description': 'Test',
'price': '99.99',
'stock': 10,
}
response = authenticated_api_client.post(url, data)
assert response.status_code == status.HTTP_201_CREATED
assert response.data['name'] == 'Test Product'
def test_update_product(self, authenticated_api_client, db):
"""è£œåæŽæ°ããã¹ãã"""
product = ProductFactory(created_by=authenticated_api_client.user)
url = reverse('api:product-detail', kwargs={'pk': product.id})
data = {'name': 'Updated Product'}
response = authenticated_api_client.patch(url, data)
assert response.status_code == status.HTTP_200_OK
assert response.data['name'] == 'Updated Product'
def test_delete_product(self, authenticated_api_client, db):
"""補ååé€ããã¹ãã"""
product = ProductFactory(created_by=authenticated_api_client.user)
url = reverse('api:product-detail', kwargs={'pk': product.id})
response = authenticated_api_client.delete(url)
assert response.status_code == status.HTTP_204_NO_CONTENT
def test_filter_products_by_price(self, api_client, db):
"""äŸ¡æ Œã«ãã補åãã£ã«ã¿ãªã³ã°ããã¹ãã"""
ProductFactory(price=50)
ProductFactory(price=150)
url = reverse('api:product-list')
response = api_client.get(url, {'price_min': 100})
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 1
def test_search_products(self, api_client, db):
"""è£œåæ€çŽ¢ããã¹ãã"""
ProductFactory(name='Apple iPhone')
ProductFactory(name='Samsung Galaxy')
url = reverse('api:product-list')
response = api_client.get(url, {'search': 'Apple'})
assert response.status_code == status.HTTP_200_OK
assert response.data['count'] == 1
ã¢ããã³ã°ãšãããã³ã°
å€éšãµãŒãã¹ã®ã¢ãã¯
# tests/test_views.py
from unittest.mock import patch, Mock
import pytest
class TestPaymentView:
"""ã¢ãã¯ãããæ±ºæžã²ãŒããŠã§ã€ã§æ±ºæžãã¥ãŒããã¹ãã"""
@patch('apps.payments.services.stripe')
def test_successful_payment(self, mock_stripe, client, user, product):
"""ã¢ãã¯ãããStripeã§æåããæ±ºæžããã¹ãã"""
# ã¢ãã¯ãèšå®
mock_stripe.Charge.create.return_value = {
'id': 'ch_123',
'status': 'succeeded',
'amount': 9999,
}
client.force_login(user)
response = client.post(reverse('payments:process'), {
'product_id': product.id,
'token': 'tok_visa',
})
assert response.status_code == 302
mock_stripe.Charge.create.assert_called_once()
@patch('apps.payments.services.stripe')
def test_failed_payment(self, mock_stripe, client, user, product):
"""倱æããæ±ºæžããã¹ãã"""
mock_stripe.Charge.create.side_effect = Exception('Card declined')
client.force_login(user)
response = client.post(reverse('payments:process'), {
'product_id': product.id,
'token': 'tok_visa',
})
assert response.status_code == 302
assert 'error' in response.url
ã¡ãŒã«éä¿¡ã®ã¢ãã¯
# tests/test_email.py
from django.core import mail
from django.test import override_settings
@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
def test_order_confirmation_email(db, order):
"""泚æç¢ºèªã¡ãŒã«ããã¹ãã"""
order.send_confirmation_email()
assert len(mail.outbox) == 1
assert order.user.email in mail.outbox[0].to
assert 'Order Confirmation' in mail.outbox[0].subject
çµ±åãã¹ã
å®å šãããŒãã¹ã
# tests/test_integration.py
import pytest
from django.urls import reverse
from tests.factories import UserFactory, ProductFactory
class TestCheckoutFlow:
"""å®å
šãªãã§ãã¯ã¢ãŠããããŒããã¹ãã"""
def test_guest_to_purchase_flow(self, client, db):
"""ã²ã¹ããã賌å
¥ãŸã§ã®å®å
šãªãããŒããã¹ãã"""
# ã¹ããã1: ç»é²
response = client.post(reverse('users:register'), {
'email': 'test@example.com',
'password': 'testpass123',
'password_confirm': 'testpass123',
})
assert response.status_code == 302
# ã¹ããã2: ãã°ã€ã³
response = client.post(reverse('users:login'), {
'email': 'test@example.com',
'password': 'testpass123',
})
assert response.status_code == 302
# ã¹ããã3: 補åãé²èЧ
product = ProductFactory(price=100)
response = client.get(reverse('products:detail', kwargs={'slug': product.slug}))
assert response.status_code == 200
# ã¹ããã4: ã«ãŒãã«è¿œå
response = client.post(reverse('cart:add'), {
'product_id': product.id,
'quantity': 1,
})
assert response.status_code == 302
# ã¹ããã5: ãã§ãã¯ã¢ãŠã
response = client.get(reverse('checkout:review'))
assert response.status_code == 200
assert product.name in response.content.decode()
# ã¹ããã6: 賌å
¥ãå®äº
with patch('apps.checkout.services.process_payment') as mock_payment:
mock_payment.return_value = True
response = client.post(reverse('checkout:complete'))
assert response.status_code == 302
assert Order.objects.filter(user__email='test@example.com').exists()
ãã¹ãã®ãã¹ããã©ã¯ãã£ã¹
ãã¹ãããš
- ãã¡ã¯ããªãŒã䜿çš: æåãªããžã§ã¯ãäœæã®ä»£ããã«
- ãã¹ãããšã«1ã€ã®ã¢ãµãŒã·ã§ã³: ãã¹ããçŠç¹ãçµã
- 説æçãªãã¹ãå:
test_user_cannot_delete_others_post - ãšããžã±ãŒã¹ããã¹ã: 空ã®å ¥åãNoneå€ãå¢çæ¡ä»¶
- å€éšãµãŒãã¹ãã¢ãã¯: å€éšAPIã«äŸåããªã
- ãã£ã¯ã¹ãã£ã䜿çš: éè€ãæé€
- ããŒããã·ã§ã³ããã¹ã: èªå¯ãæ©èœããããšã確èª
- ãã¹ããé«éã«ä¿ã€:
--reuse-dbãš--nomigrationsã䜿çš
ãã¹ãã§ãªãããš
- Djangoå éšããã¹ãããªã: Djangoãæ©èœããããšãä¿¡é Œ
- ãµãŒãããŒãã£ã³ãŒãããã¹ãããªã: ã©ã€ãã©ãªãæ©èœããããšãä¿¡é Œ
- 倱æãããã¹ããç¡èŠããªã: ãã¹ãŠã®ãã¹ããéãå¿ èŠããã
- ãã¹ããäŸåãããªã: ãã¹ãã¯ä»»æã®é åºã§å®è¡ã§ããã¹ã
- é床ã«ã¢ãã¯ããªã: å€éšäŸåé¢ä¿ã®ã¿ãã¢ãã¯
- ãã©ã€ããŒãã¡ãœããããã¹ãããªã: ãããªãã¯ã€ã³ã¿ãŒãã§ãŒã¹ããã¹ã
- æ¬çªããŒã¿ããŒã¹ã䜿çšããªã: åžžã«ãã¹ãããŒã¿ããŒã¹ã䜿çš
ã«ãã¬ããž
ã«ãã¬ããžèšå®
# ã«ãã¬ããžã§ãã¹ããå®è¡
pytest --cov=apps --cov-report=html --cov-report=term-missing
# HTMLã¬ããŒããçæ
open htmlcov/index.html
ã«ãã¬ããžç®æš
| ã³ã³ããŒãã³ã | ç®æšã«ãã¬ããž |
|---|---|
| ã¢ãã« | 90%+ |
| ã·ãªã¢ã©ã€ã¶ãŒ | 85%+ |
| ãã¥ãŒ | 80%+ |
| ãµãŒãã¹ | 90%+ |
| ãŠãŒãã£ãªã㣠| 80%+ |
| å šäœ | 80%+ |
ã¯ã€ãã¯ãªãã¡ã¬ã³ã¹
| ãã¿ãŒã³ | äœ¿çšæ³ |
|---|---|
@pytest.mark.django_db |
ããŒã¿ããŒã¹ã¢ã¯ã»ã¹ãæå¹å |
client |
Djangoãã¹ãã¯ã©ã€ã¢ã³ã |
api_client |
DRF APIã¯ã©ã€ã¢ã³ã |
factory.create_batch(n) |
è€æ°ã®ãªããžã§ã¯ããäœæ |
patch('module.function') |
å€éšäŸåé¢ä¿ãã¢ã㯠|
override_settings |
èšå®ãäžæçã«å€æŽ |
force_authenticate() |
ãã¹ãã§èªèšŒããã€ãã¹ |
assertRedirects |
ãªãã€ã¬ã¯ãããã§ã㯠|
assertTemplateUsed |
ãã³ãã¬ãŒã䜿çšãæ€èšŒ |
mail.outbox |
éä¿¡ãããã¡ãŒã«ããã§ã㯠|
èŠããŠãããŠãã ãã: ãã¹ãã¯ããã¥ã¡ã³ãã§ããè¯ããã¹ãã¯ã³ãŒããã©ã®ããã«åäœãã¹ããã説æããŸããã·ã³ãã«ã§ãèªã¿ããããä¿å®å¯èœã«ä¿ã£ãŠãã ããã
Install
Add Django Tdd to your client. Pick the one you use.
npx skills add edp43273-glitch/181-skillsInstalls every skill in the repository, then prompts for which to keep.
/plugin marketplace add edp43273-glitch/181-skillsAdds the repository as a plugin marketplace; install individual plugins with `/plugin install`.
git clone https://github.com/edp43273-glitch/181-skills
cp -r docs/ja-JP/skills/django-tdd ~/.claude/skills/A skill is a plain directory. Copy it into `.claude/skills/` in a project or in your home directory.
Score
76 / 100
Good