Django Tdd

affaan-m/ECC/docs/zh-CN/skills/django-tdd

by affaan-mef648e01899ba3e8dc6371642deaaf64b4477775No license275K starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated 4 days ago

Django 测试策略,包括 pytest-django、TDD 方法、factory_boy、模拟、覆盖率以及测试 Django REST Framework API。

Instructions onlySoftware Development
AI-generated overview

Guides test-driven development for Django apps with pytest, factory_boy and DRF API testing.

What it does
This skill provides a Django testing playbook covering the red-green-refactor TDD cycle, pytest configuration, test settings, and conftest fixtures. It shows how to build factory_boy factories and write tests for models, views, serializers, and Django REST Framework API endpoints, including mocking external services and email. It also covers integration flow tests, coverage configuration and targets, and testing best practices.
When to use it
Use it when starting a new Django application, implementing Django REST Framework APIs, or testing Django models, views and serializers. It also fits setting up test infrastructure such as pytest, fixtures and factories for a Django project.
Requirements
Requires a Django project with pytest, pytest-django, factory_boy, and Django REST Framework for API tests; coverage tooling is used for coverage reports. It ships no scripts and is instructions only.

使用 TDD 进行 Django 测试

使用 pytest、factory_boy 和 Django REST Framework 进行 Django 应用程序的测试驱动开发。

何时激活

  • 编写新的 Django 应用程序时
  • 实现 Django REST Framework API 时
  • 测试 Django 模型、视图和序列化器时
  • 为 Django 项目设置测试基础设施时

Django 的 TDD 工作流

红-绿-重构循环

python
# Step 1: RED - Write failing testdef 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
# Step 2: GREEN - Make test pass# Create User model or factory
# Step 3: REFACTOR - Improve while keeping tests green

设置

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:',    }}
# Disable migrations for speedclass DisableMigrations:    def __contains__(self, item):        return True
    def __getitem__(self, item):        return None
MIGRATION_MODULES = DisableMigrations()
# Faster password hashingPASSWORD_HASHERS = [    'django.contrib.auth.hashers.MD5PasswordHasher',]
# Email backendEMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Celery always 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):    """Ensure consistent timezone."""    settings.TIME_ZONE = 'UTC'
@pytest.fixturedef user(db):    """Create a test user."""    return User.objects.create_user(        email='[email protected]',        password='testpass123',        username='testuser'    )
@pytest.fixturedef admin_user(db):    """Create an admin user."""    return User.objects.create_superuser(        email='[email protected]',        password='adminpass123',        username='admin'    )
@pytest.fixturedef authenticated_client(client, user):    """Return authenticated client."""    client.force_login(user)    return client
@pytest.fixturedef api_client():    """Return DRF API client."""    from rest_framework.test import APIClient    return APIClient()
@pytest.fixturedef authenticated_api_client(api_client, user):    """Return authenticated API client."""    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):    """Factory for User model."""
    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):    """Factory for Category model."""
    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):    """Factory for Product model."""
    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):        """Add tags to product."""        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():    """Test product creation using factory."""    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():    """Test product with tags."""    tags = [TagFactory(name='electronics'), TagFactory(name='new')]    product = ProductFactory(tags=tags)    assert product.tags.count() == 2
def test_multiple_products():    """Test creating 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:    """Test User model."""
    def test_create_user(self, db):        """Test creating a regular user."""        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):        """Test creating a superuser."""        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):        """Test user string representation."""        user = UserFactory(email='[email protected]')        assert str(user) == '[email protected]'
class TestProductModel:    """Test Product model."""
    def test_product_creation(self, db):        """Test creating a product."""        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):        """Test automatic slug generation."""        product = ProductFactory(name='Test Product')        assert product.slug == 'test-product'
    def test_product_price_validation(self, db):        """Test price cannot be negative."""        product = ProductFactory(price=-10)        with pytest.raises(ValidationError):            product.full_clean()
    def test_product_manager_active(self, db):        """Test active manager method."""        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):        """Test stock management."""        product = ProductFactory(stock=10)        product.reduce_stock(5)        product.refresh_from_db()        assert product.stock == 5
        with pytest.raises(ValueError):            product.reduce_stock(10)  # Not enough stock

视图测试

Django 视图测试

python
# tests/test_views.pyimport pytestfrom django.urls import reversefrom tests.factories import ProductFactory, UserFactory
class TestProductViews:    """Test product views."""
    def test_product_list(self, client, db):        """Test product list view."""        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):        """Test product detail view."""        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):        """Test product creation requires authentication."""        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):        """Test product creation as authenticated user."""        response = authenticated_client.get(reverse('products:create'))
        assert response.status_code == 200
    def test_product_create_post(self, authenticated_client, db, category):        """Test creating a product via 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:    """Test ProductSerializer."""
    def test_serialize_product(self, db):        """Test serializing a product."""        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):        """Test deserializing product data."""        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):        """Test price validation."""        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):        """Test stock cannot be negative."""        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:    """Test Product API endpoints."""
    @pytest.fixture    def api_client(self):        """Return API client."""        return APIClient()
    def test_list_products(self, api_client, db):        """Test listing products."""        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):        """Test retrieving a product."""        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):        """Test creating product without authentication."""        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):        """Test creating product as authenticated user."""        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):        """Test updating a product."""        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):        """Test deleting a product."""        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):        """Test filtering products by price."""        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):        """Test searching products."""        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:    """Test payment view with mocked payment gateway."""
    @patch('apps.payments.services.stripe')    def test_successful_payment(self, mock_stripe, client, user, product):        """Test successful payment with mocked Stripe."""        # Configure mock        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):        """Test failed payment."""        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):    """Test order confirmation email."""    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:    """Test complete checkout flow."""
    def test_guest_to_purchase_flow(self, client, db):        """Test complete flow from guest to purchase."""        # Step 1: Register        response = client.post(reverse('users:register'), {            'email': '[email protected]',            'password': 'testpass123',            'password_confirm': 'testpass123',        })        assert response.status_code == 302
        # Step 2: Login        response = client.post(reverse('users:login'), {            'email': '[email protected]',            'password': 'testpass123',        })        assert response.status_code == 302
        # Step 3: Browse products        product = ProductFactory(price=100)        response = client.get(reverse('products:detail', kwargs={'slug': product.slug}))        assert response.status_code == 200
        # Step 4: Add to cart        response = client.post(reverse('cart:add'), {            'product_id': product.id,            'quantity': 1,        })        assert response.status_code == 302
        # Step 5: Checkout        response = client.get(reverse('checkout:review'))        assert response.status_code == 200        assert product.name in response.content.decode()
        # Step 6: Complete purchase        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()

测试最佳实践

应该做

  • 使用工厂:而不是手动创建对象
  • 每个测试一个断言:保持测试聚焦
  • 描述性测试名称:test_user_cannot_delete_others_post
  • 测试边界情况:空输入、None 值、边界条件
  • 模拟外部服务:不要依赖外部 API
  • 使用夹具:消除重复
  • 测试权限:确保授权有效
  • 保持测试快速:使用 --reuse-db 和 --nomigrations

不应该做

  • 不要测试 Django 内部:相信 Django 能正常工作
  • 不要测试第三方代码:相信库能正常工作
  • 不要忽略失败的测试:所有测试必须通过
  • 不要让测试产生依赖:测试应该能以任何顺序运行
  • 不要过度模拟:只模拟外部依赖
  • 不要测试私有方法:测试公共接口
  • 不要使用生产数据库:始终使用测试数据库

覆盖率

覆盖率配置

bash
# Run tests with coveragepytest --cov=apps --cov-report=html --cov-report=term-missing
# Generate HTML reportopen 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检查已发送的邮件

记住:测试即文档。好的测试解释了你的代码应如何工作。保持测试简单、可读和可维护。

Source and attribution

Source:affaan-m/ECCindocs/zh-CN/skills/django-tddat commitef648e0

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal