<< All versions
Skill v1.0.1
currentAutomated scan96/100xu-xiang/everything-claude-code-zh/django-tdd
4 files
──Details
PublishedMay 21, 2026 at 01:49 AM
Content Hashsha256:b8380028921564c2...
Git SHAdfbf9467b90d
Bump Typepatch
──Files
Files (1 file, 20.8 KB)
SKILL.md20.8 KBactive
SKILL.md · 731 lines · 20.8 KB
version: "1.0.1" name: django-tdd description: 使用 pytest-django 进行 Django 测试的策略、TDD 方法论、factory_boy、Mock 模拟、测试覆盖率以及测试 Django REST Framework API。 origin: ECC
使用 TDD 进行 Django 测试
使用 pytest、factory_boy 和 Django REST Framework 对 Django 应用程序进行测试驱动开发(TDD)。
何时激活
- 编写新的 Django 应用程序时
- 实现 Django REST Framework API 时
- 测试 Django 模型(Models)、视图(Views)和序列化器(Serializers)时
- 为 Django 项目搭建测试基础设施时
Django 的 TDD 工作流
红-绿-重构(Red-Green-Refactor)周期
python
# 步骤 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 配置
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: 标记为慢速测试integration: 标记为集成测试
测试设置(Settings)
python
# config/settings/test.pyfrom .base import *DEBUG = TrueDATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3','NAME': ':memory:',}}# 禁用迁移以提高速度class DisableMigrations:def __contains__(self, item):return Truedef __getitem__(self, item):return NoneMIGRATION_MODULES = DisableMigrations()# 更快的密码哈希算法PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher',]# 邮件后端EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'# Celery 设置为同步执行CELERY_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_modelUser = 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='test@example.com',password='testpass123',username='testuser')@pytest.fixturedef admin_user(db):"""创建一个管理员用户。"""return User.objects.create_superuser(email='admin@example.com',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 APIClientreturn 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, CategoryUser = get_user_model()class UserFactory(factory.django.DjangoModelFactory):"""User 模型的工厂类。"""class Meta:model = Useremail = 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 = Trueclass CategoryFactory(factory.django.DjangoModelFactory):"""Category 模型的工厂类。"""class Meta:model = Categoryname = factory.Faker('word')slug = factory.LazyAttribute(lambda obj: obj.name.lower())description = factory.Faker('text')class ProductFactory(factory.django.DjangoModelFactory):"""Product 模型的工厂类。"""class Meta:model = Productname = 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 = Truecategory = factory.SubFactory(CategoryFactory)created_by = factory.SubFactory(UserFactory)@factory.post_generationdef tags(self, create, extracted, **kwargs):"""为产品添加标签。"""if not create:returnif extracted:for tag in extracted:self.tags.add(tag)
使用工厂
python
# tests/test_models.pyimport pytestfrom tests.factories import ProductFactory, UserFactorydef test_product_creation():"""测试使用工厂创建产品。"""product = ProductFactory(price=100.00, stock=50)assert product.price == 100.00assert product.stock == 50assert product.is_active is Truedef test_product_with_tags():"""测试带有标签的产品。"""tags = [TagFactory(name='electronics'), TagFactory(name='new')]product = ProductFactory(tags=tags)assert product.tags.count() == 2def test_multiple_products():"""测试创建多个产品。"""products = ProductFactory.create_batch(10)assert len(products) == 10
模型(Model)测试
模型测试用例
python
# tests/test_models.pyimport pytestfrom django.core.exceptions import ValidationErrorfrom tests.factories import UserFactory, ProductFactoryclass 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_staffassert not user.is_superuserdef test_create_superuser(self, db):"""测试创建超级用户。"""user = UserFactory(email='admin@example.com',is_staff=True,is_superuser=True)assert user.is_staffassert user.is_superuserdef 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 Noneassert product.is_active is Trueassert product.created_at is not Nonedef test_product_slug_generation(self, db):"""测试自动生成 slug。"""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):"""测试 active 管理器方法。"""ProductFactory.create_batch(5, is_active=True)ProductFactory.create_batch(3, is_active=False)active_count = Product.objects.active().count()assert active_count == 5def test_product_stock_management(self, db):"""测试库存管理。"""product = ProductFactory(stock=10)product.reduce_stock(5)product.refresh_from_db()assert product.stock == 5with pytest.raises(ValueError):product.reduce_stock(10) # 库存不足
视图(View)测试
Django 视图测试
python
# tests/test_views.pyimport pytestfrom django.urls import reversefrom tests.factories import ProductFactory, UserFactoryclass TestProductViews:"""测试产品视图。"""def test_product_list(self, client, db):"""测试产品列表视图。"""ProductFactory.create_batch(10)response = client.get(reverse('products:list'))assert response.status_code == 200assert len(response.context['products']) == 10def test_product_detail(self, client, db):"""测试产品详情视图。"""product = ProductFactory()response = client.get(reverse('products:detail', kwargs={'slug': product.slug}))assert response.status_code == 200assert response.context['product'] == productdef test_product_create_requires_login(self, client, db):"""测试创建产品需要身份认证。"""response = client.get(reverse('products:create'))assert response.status_code == 302assert 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 == 200def 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 == 302assert Product.objects.filter(name='Test Product').exists()
DRF API 测试
序列化器(Serializer)测试
python
# tests/test_serializers.pyimport pytestfrom rest_framework.exceptions import ValidationErrorfrom apps.products.serializers import ProductSerializerfrom tests.factories import ProductFactoryclass TestProductSerializer:"""测试 ProductSerializer。"""def test_serialize_product(self, db):"""测试序列化产品。"""product = ProductFactory()serializer = ProductSerializer(product)data = serializer.dataassert data['id'] == product.idassert data['name'] == product.nameassert 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.99def 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.errorsdef 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, UserFactoryclass TestProductAPI:"""测试产品 API 端点。"""@pytest.fixturedef 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_OKassert response.data['count'] == 10def 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_OKassert response.data['id'] == product.iddef 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_UNAUTHORIZEDdef 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_CREATEDassert 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_OKassert 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_CONTENTdef 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_OKassert response.data['count'] == 1def 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_OKassert response.data['count'] == 1
Mock 模拟与打补丁(Patching)
模拟外部服务
python
# tests/test_views.pyfrom unittest.mock import patch, Mockimport pytestclass TestPaymentView:"""测试使用了 Mock 模拟支付网关的支付视图。"""@patch('apps.payments.services.stripe')def test_successful_payment(self, mock_stripe, client, user, product):"""测试使用 Mock Stripe 的成功支付流程。"""# 配置 Mockmock_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 == 302mock_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 == 302assert '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) == 1assert order.user.email in mail.outbox[0].toassert 'Order Confirmation' in mail.outbox[0].subject
集成测试
全流程测试
python
# tests/test_integration.pyimport pytestfrom django.urls import reversefrom tests.factories import UserFactory, ProductFactoryclass 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 == 200assert product.name in response.content.decode()# 步骤 6: 完成购买with patch('apps.checkout.services.process_payment') as mock_payment:mock_payment.return_value = Trueresponse = client.post(reverse('checkout:complete'))assert response.status_code == 302assert Order.objects.filter(user__email='test@example.com').exists()
测试最佳实践
应该(DO)
- 使用工厂(Factories): 而不是手动创建对象
- 每个测试一个断言: 保持测试专注
- 使用描述性的测试名称: 例如
test_user_cannot_delete_others_post - 测试边缘情况: 空输入、None 值、边界条件
- 模拟外部服务: 不要依赖外部 API
- 使用 Fixtures: 消除重复
- 测试权限: 确保授权逻辑正常工作
- 保持测试快速: 使用
--reuse-db和--nomigrations
不该(DON'T)
- 不要测试 Django 内部机制: 相信 Django 本身是正常的
- 不要测试第三方库代码: 相信第三方库是正常的
- 不要忽略失败的测试: 所有测试必须通过
- 不要使测试产生依赖: 测试应该能以任何顺序运行
- 不要过度使用 Mock: 仅模拟外部依赖
- 不要测试私有方法: 只测试公共接口
- 不要使用生产数据库: 始终使用测试数据库
覆盖率(Coverage)
覆盖率配置
bash
# 运行带有覆盖率报告的测试pytest --cov=apps --cov-report=html --cov-report=term-missing# 生成 HTML 报告open htmlcov/index.html
覆盖率目标
| 组件 | 目标覆盖率 | |
|---|---|---|
| 模型 (Models) | 90%+ | |
| 序列化器 (Serializers) | 85%+ | |
| 视图 (Views) | 80%+ | |
| 服务 (Services) | 90%+ | |
| 工具类 (Utilities) | 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 | 检查已发送的邮件 |
记住:测试即文档。良好的测试能够解释代码应该如何工作。保持测试简单、易读且易于维护。