
关于
Django 测试策略,涵盖 pytest-django、TDD 方法论、factory_boy、Mock、覆盖率和 Django REST Framework API 测试。
name: django-tdd description: 使用 pytest-django 的 Django 测试策略、TDD 方法论、factory_boy、模拟、覆盖率和 Django REST Framework API 测试。 origin: ECC
Django TDD 测试
使用 pytest、factory_boy 和 Django REST Framework 进行 Django 应用的测试驱动开发。
激活时机
- 编写新的 Django 应用
- 实现 Django REST Framework API
- 测试 Django 模型、视图和序列化器
- 为 Django 项目设置测试基础设施
Django 的 TDD 工作流
红-绿-重构循环
# Step 1: RED - Write failing test
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
# Step 2: GREEN - Make test pass
# Create User model or factory
# Step 3: REFACTOR - Improve while keeping tests green
设置
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:',
}
}
# Disable migrations for speed
class DisableMigrations:
def __contains__(self, item):
return True
def __getitem__(self, item):
return None
MIGRATION_MODULES = DisableMigrations()
# Faster password hashing
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
# Email backend
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Celery always 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):
"""Ensure consistent timezone."""
settings.TIME_ZONE = 'UTC'
@pytest.fixture
def user(db):
"""Create a test user."""
return User.objects.create_user(
email='test@example.com',
password='testpass123',
username='testuser'
)
@pytest.fixture
def admin_user(db):
"""Create an admin user."""
return User.objects.create_superuser(
email='admin@example.com',
password='adminpass123',
username='admin'
)
@pytest.fixture
def authenticated_client(client, user):
"""Return authenticated client."""
client.force_login(user)
return client
@pytest.fixture
def api_client():
"""Return DRF API client."""
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture
def authenticated_api_client(api_client, user):
"""Return authenticated API client."""
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):
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):
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):
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)
测试模型
# tests/test_models.py
import pytest
from tests.factories import ProductFactory, UserFactory
@pytest.mark.django_db
class TestProductModel:
def test_create_product(self):
product = ProductFactory()
assert product.pk is not None
assert product.is_active is True
def test_product_str(self):
product = ProductFactory(name="Test Product")
assert str(product) == "Test Product"
def test_product_slug_generation(self):
product = ProductFactory(name="My Great Product")
assert product.slug == "my-great-product"
测试 DRF API
# tests/test_api.py
import pytest
from rest_framework import status
from tests.factories import ProductFactory, UserFactory
@pytest.mark.django_db
class TestProductAPI:
def test_list_products(self, authenticated_api_client):
ProductFactory.create_batch(3)
response = authenticated_api_client.get('/api/v1/products/')
assert response.status_code == status.HTTP_200_OK
assert len(response.data['results']) == 3
def test_create_product(self, authenticated_api_client):
data = {
'name': 'New Product',
'price': '29.99',
'stock': 10,
}
response = authenticated_api_client.post('/api/v1/products/', data)
assert response.status_code == status.HTTP_201_CREATED
def test_unauthenticated_access(self, api_client):
response = api_client.get('/api/v1/products/')
assert response.status_code == status.HTTP_401_UNAUTHORIZED
模拟
# tests/test_services.py
import pytest
from unittest.mock import patch, MagicMock
from apps.orders.services import OrderService
@pytest.mark.django_db
class TestOrderService:
@patch('apps.orders.services.stripe.Charge.create')
def test_process_payment(self, mock_stripe):
mock_stripe.return_value = MagicMock(id='ch_123', status='succeeded')
result = OrderService.process_payment(amount=1000, token='tok_visa')
assert result['status'] == 'succeeded'
mock_stripe.assert_called_once()
@patch('apps.orders.services.send_mail')
def test_send_confirmation(self, mock_mail):
OrderService.send_confirmation(order_id=1)
mock_mail.assert_called_once()
覆盖率
# Run with coverage
pytest --cov=apps --cov-report=html --cov-report=term-missing
# Check minimum coverage
pytest --cov=apps --cov-fail-under=80
最佳实践
- 每个测试一个断言 - 保持测试聚焦
- 使用工厂而非 fixtures - 更灵活、更易维护
- 测试行为而非实现 - 关注输入/输出
- 使用
pytest.mark.django_db- 明确标记数据库测试 - 模拟外部服务 - 隔离测试、加快速度
- 使用内存数据库 - 测试运行更快
- 遵循 AAA 模式 - Arrange、Act、Assert
兼容工具
Claude CodeCursor
标签
测试

