Django Tdd

作者 affaan-mef648e01899b无许可证275K 个星标收录于 2026年10月8日更新于 2026年10月8日仓库3天前更新

Django testing strategies with pytest-django, TDD methodology, factory_boy, mocking, coverage, and testing Django REST Framework APIs.

AI 生成的概览

指导 Django 应用的测试驱动开发,涵盖 pytest、factory_boy、mock、覆盖率与 DRF API 测试。

功能
该技能提供在 Django 项目中编写测试的参考指南,采用红-绿-重构的 TDD 工作流。内容涵盖 pytest 配置、测试设置、conftest 夹具、factory_boy 工厂,以及模型、视图、序列化器和 Django REST Framework API 测试,还包括外部服务与邮件的 mock、集成流程、最佳实践和覆盖率目标。它产出的是示例测试代码和配置片段,而不是可运行脚本。
适用场景
适用于开始编写新的 Django 应用、实现 Django REST Framework API,或测试 Django 模型、视图和序列化器时。也适合为 Django 项目搭建 pytest 设置、夹具和工厂等测试基础设施。
运行要求
不附带脚本,仅为说明和代码示例。按此实践需要具备 Django 项目,并安装 pytest、pytest-django、factory_boy、coverage 和 Django REST Framework,同时配置测试数据库。

Django テスト駆動開発(TDD)

pytest、factory_boy、Django REST Frameworkを使用したDjangoアプリケーションのテスト駆動開発。

いつ有効化するか

  • 新しいDjangoアプリケーションを書くとき
  • Django REST Framework APIを実装するとき
  • Djangoモデル、ビュー、シリアライザーをテストするとき
  • Djangoプロジェクトのテストインフラを設定するとき

DjangoのためのTDDワークフロー

Red-Green-Refactorサイクル

python
# ステップ1: RED - 失敗するテストを書くdef test_user_creation():    user = User.objects.create_user(email='[email protected]', password='testpass123')    assert user.email == '[email protected]'    assert user.check_password('testpass123')    assert not user.is_staff
# ステップ2: GREEN - テストを通す# Userモデルまたはファクトリーを作成
# ステップ3: REFACTOR - テストをグリーンに保ちながら改善

セットアップ

pytest設定

ini
# pytest.ini[pytest]DJANGO_SETTINGS_MODULE = config.settings.testtestpaths = testspython_files = test_*.pypython_classes = Test*python_functions = test_*addopts =    --reuse-db    --nomigrations    --cov=apps    --cov-report=html    --cov-report=term-missing    --strict-markersmarkers =    slow: marks tests as slow    integration: marks tests as integration tests

テスト設定

python
# config/settings/test.pyfrom .base import *
DEBUG = TrueDATABASES = {    '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は常にeagerCELERY_TASK_ALWAYS_EAGER = TrueCELERY_TASK_EAGER_PROPAGATES = True

conftest.py

python
# tests/conftest.pyimport pytestfrom django.utils import timezonefrom django.contrib.auth import get_user_model
User = get_user_model()
@pytest.fixture(autouse=True)def timezone_settings(settings):    """一貫したタイムゾーンを確保。"""    settings.TIME_ZONE = 'UTC'
@pytest.fixturedef user(db):    """テストユーザーを作成。"""    return User.objects.create_user(        email='[email protected]',        password='testpass123',        username='testuser'    )
@pytest.fixturedef admin_user(db):    """管理者ユーザーを作成。"""    return User.objects.create_superuser(        email='[email protected]',        password='adminpass123',        username='admin'    )
@pytest.fixturedef authenticated_client(client, user):    """認証済みクライアントを返す。"""    client.force_login(user)    return client
@pytest.fixturedef api_client():    """DRF APIクライアントを返す。"""    from rest_framework.test import APIClient    return APIClient()
@pytest.fixturedef authenticated_api_client(api_client, user):    """認証済みAPIクライアントを返す。"""    api_client.force_authenticate(user=user)    return api_client

Factory Boy

ファクトリーセットアップ

python
# tests/factories.pyimport factoryfrom factory import fuzzyfrom datetime import datetime, timedeltafrom django.contrib.auth import get_user_modelfrom 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)

ファクトリーの使用

python
# tests/test_models.pyimport pytestfrom 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

モデルテスト

モデルテスト

python
# tests/test_models.pyimport pytestfrom django.core.exceptions import ValidationErrorfrom tests.factories import UserFactory, ProductFactory
class TestUserModel:    """Userモデルをテスト。"""
    def test_create_user(self, db):        """通常のユーザー作成をテスト。"""        user = UserFactory(email='[email protected]')        assert user.email == '[email protected]'        assert user.check_password('testpass123')        assert not user.is_staff        assert not user.is_superuser
    def test_create_superuser(self, db):        """スーパーユーザー作成をテスト。"""        user = UserFactory(            email='[email protected]',            is_staff=True,            is_superuser=True        )        assert user.is_staff        assert user.is_superuser
    def test_user_str(self, db):        """ユーザーの文字列表現をテスト。"""        user = UserFactory(email='[email protected]')        assert str(user) == '[email protected]'
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ビューテスト

python
# tests/test_views.pyimport pytestfrom django.urls import reversefrom 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テスト

シリアライザーテスト

python
# tests/test_serializers.pyimport pytestfrom rest_framework.exceptions import ValidationErrorfrom apps.products.serializers import ProductSerializerfrom 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テスト

python
# tests/test_api.pyimport pytestfrom rest_framework.test import APIClientfrom rest_framework import statusfrom django.urls import reversefrom 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

モッキングとパッチング

外部サービスのモック

python
# tests/test_views.pyfrom unittest.mock import patch, Mockimport 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

メール送信のモック

python
# tests/test_email.pyfrom django.core import mailfrom 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

統合テスト

完全フローテスト

python
# tests/test_integration.pyimport pytestfrom django.urls import reversefrom tests.factories import UserFactory, ProductFactory
class TestCheckoutFlow:    """完全なチェックアウトフローをテスト。"""
    def test_guest_to_purchase_flow(self, client, db):        """ゲストから購入までの完全なフローをテスト。"""        # ステップ1: 登録        response = client.post(reverse('users:register'), {            'email': '[email protected]',            'password': 'testpass123',            'password_confirm': 'testpass123',        })        assert response.status_code == 302
        # ステップ2: ログイン        response = client.post(reverse('users:login'), {            'email': '[email protected]',            '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='[email protected]').exists()

テストのベストプラクティス

すべきこと

  • ファクトリーを使用: 手動オブジェクト作成の代わりに
  • テストごとに1つのアサーション: テストを焦点を絞る
  • 説明的なテスト名: test_user_cannot_delete_others_post
  • エッジケースをテスト: 空の入力、None値、境界条件
  • 外部サービスをモック: 外部APIに依存しない
  • フィクスチャを使用: 重複を排除
  • パーミッションをテスト: 認可が機能することを確認
  • テストを高速に保つ: --reuse-dbと--nomigrationsを使用

すべきでないこと

  • Django内部をテストしない: Djangoが機能することを信頼
  • サードパーティコードをテストしない: ライブラリが機能することを信頼
  • 失敗するテストを無視しない: すべてのテストが通る必要がある
  • テストを依存させない: テストは任意の順序で実行できるべき
  • 過度にモックしない: 外部依存関係のみをモック
  • プライベートメソッドをテストしない: パブリックインターフェースをテスト
  • 本番データベースを使用しない: 常にテストデータベースを使用

カバレッジ

カバレッジ設定

bash
# カバレッジでテストを実行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データベースアクセスを有効化
clientDjangoテストクライアント
api_clientDRF APIクライアント
factory.create_batch(n)複数のオブジェクトを作成
patch('module.function')外部依存関係をモック
override_settings設定を一時的に変更
force_authenticate()テストで認証をバイパス
assertRedirectsリダイレクトをチェック
assertTemplateUsedテンプレート使用を検証
mail.outbox送信されたメールをチェック

覚えておいてください: テストはドキュメントです。良いテストはコードがどのように動作すべきかを説明します。シンプルで、読みやすく、保守可能に保ってください。

来源与署名

来源:affaan-m/ecc位于docs/ja-JP/skills/django-tdd提交ef648e0

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架