django-dev-test
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDjango Testing Patterns
Django 测试模式
pytest-django testing with factory_boy for fixture management.
结合factory_boy的pytest-django测试,用于fixture管理。
Core Principles
核心原则
- pytest only - Never use Django's TestCase
- factory_boy - Use factories for all test data
- Mirror structure - Tests mirror app structure
- Isolation - Each test fully isolated
- Fast fixtures - Prefer over setUp
@pytest.fixture
- 仅使用pytest - 绝不使用Django的TestCase
- factory_boy - 所有测试数据均使用工厂生成
- 镜像结构 - 测试目录与应用结构保持一致
- 隔离性 - 每个测试完全独立
- 快速Fixture - 优先使用而非setUp
@pytest.fixture
Installation
安装
bash
pip install pytest pytest-django factory-boy pytest-covbash
pip install pytest pytest-django factory-boy pytest-covConfiguration
配置
pytest.inipyproject.tomltoml
undefinedpytest.inipyproject.tomltoml
undefinedpyproject.toml
pyproject.toml
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings"
python_files = ["test_.py", "test.py"]
python_classes = ["Test*"]
python_functions = ["test*"]
addopts = [
"--strict-markers",
"-ra",
"--tb=short",
]
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
]
undefined[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings"
python_files = ["test_.py", "test.py"]
python_classes = ["Test*"]
python_functions = ["test*"]
addopts = [
"--strict-markers",
"-ra",
"--tb=short",
]
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
]
undefinedTest Structure
测试结构
tests/
├── conftest.py # Shared fixtures
├── factories/
│ ├── __init__.py
│ ├── user.py # UserFactory
│ ├── product.py # ProductFactory
│ └── order.py # OrderFactory
├── unit/
│ ├── models/
│ │ ├── test_user.py
│ │ └── test_product.py
│ └── services/
│ └── test_user_service.py
├── integration/
│ └── api/
│ ├── test_users.py
│ └── test_products.py
└── e2e/
└── test_checkout.pytests/
├── conftest.py # 共享Fixture
├── factories/
│ ├── __init__.py
│ ├── user.py # UserFactory
│ ├── product.py # ProductFactory
│ └── order.py # OrderFactory
├── unit/
│ ├── models/
│ │ ├── test_user.py
│ │ └── test_product.py
│ └── services/
│ └── test_user_service.py
├── integration/
│ └── api/
│ ├── test_users.py
│ └── test_products.py
└── e2e/
└── test_checkout.pyConftest Setup
Conftest 配置
tests/conftest.pypython
import pytest
from django.test import Client
from ninja.testing import TestClient
from apps.myapp.api import api
@pytest.fixture
def client():
"""Django test client."""
return Client()
@pytest.fixture
def api_client():
"""Django Ninja test client."""
return TestClient(api)
@pytest.fixture
def authenticated_client(api_client, user):
"""API client with authentication."""
api_client.headers["Authorization"] = f"Bearer {user.get_token()}"
return api_client
@pytest.fixture
def user(user_factory):
"""Default test user."""
return user_factory()
@pytest.fixture
def admin_user(user_factory):
"""Admin test user."""
return user_factory(is_staff=True, is_superuser=True)tests/conftest.pypython
import pytest
from django.test import Client
from ninja.testing import TestClient
from apps.myapp.api import api
@pytest.fixture
def client():
"""Django test client."""
return Client()
@pytest.fixture
def api_client():
"""Django Ninja test client."""
return TestClient(api)
@pytest.fixture
def authenticated_client(api_client, user):
"""已认证的API客户端。"""
api_client.headers["Authorization"] = f"Bearer {user.get_token()}"
return api_client
@pytest.fixture
def user(user_factory):
"""默认测试用户。"""
return user_factory()
@pytest.fixture
def admin_user(user_factory):
"""管理员测试用户。"""
return user_factory(is_staff=True, is_superuser=True)Factory Pattern
工厂模式
Base factory in :
factories/__init__.pypython
import factory
from factory.django import DjangoModelFactory
class BaseFactory(DjangoModelFactory):
"""Base factory with common patterns."""
class Meta:
abstract = True
@classmethod
def _create(cls, model_class, *args, **kwargs):
"""Override to handle soft-deleted models."""
obj = super()._create(model_class, *args, **kwargs)
return objUser factory in :
factories/user.pypython
import factory
from factory import fuzzy
from apps.users.models import User
from . import BaseFactory
class UserFactory(BaseFactory):
"""Factory for User model."""
class Meta:
model = User
skip_postgeneration_save = True
email = factory.LazyAttribute(
lambda obj: f"{obj.name.lower().replace(' ', '.')}@example.com"
)
name = factory.Faker("name")
is_active = True
@factory.post_generation
def password(obj, create, extracted, **kwargs):
password = extracted or "testpass123"
obj.set_password(password)
if create:
obj.save(update_fields=["password"])
@classmethod
def _create(cls, model_class, *args, **kwargs):
"""Create with explicit password handling."""
password = kwargs.pop("password", None)
user = super()._create(model_class, *args, **kwargs)
if password:
user.set_password(password)
user.save(update_fields=["password"])
return userOrder factory with relationships ():
factories/order.pypython
import factory
from decimal import Decimal
from apps.orders.models import Order, OrderItem
from . import BaseFactory
from .user import UserFactory
from .product import ProductFactory
class OrderFactory(BaseFactory):
"""Factory for Order model."""
class Meta:
model = Order
user = factory.SubFactory(UserFactory)
status = "pending"
total = factory.LazyAttribute(lambda obj: Decimal("0.00"))
@factory.post_generation
def items(obj, create, extracted, **kwargs):
if not create:
return
if extracted:
for item in extracted:
OrderItemFactory(order=obj, **item)
else:
# Create 1-3 random items
import random
for _ in range(random.randint(1, 3)):
OrderItemFactory(order=obj)
# Update total
obj.total = sum(item.subtotal for item in obj.items.all())
obj.save(update_fields=["total"])
class OrderItemFactory(BaseFactory):
"""Factory for OrderItem model."""
class Meta:
model = OrderItem
order = factory.SubFactory(OrderFactory)
product = factory.SubFactory(ProductFactory)
quantity = factory.fuzzy.FuzzyInteger(1, 5)
unit_price = factory.LazyAttribute(lambda obj: obj.product.price)factories/__init__.pypython
import factory
from factory.django import DjangoModelFactory
class BaseFactory(DjangoModelFactory):
"""包含通用模式的基础工厂。"""
class Meta:
abstract = True
@classmethod
def _create(cls, model_class, *args, **kwargs):
"""重写以支持软删除模型。"""
obj = super()._create(model_class, *args, **kwargs)
return objfactories/user.pypython
import factory
from factory import fuzzy
from apps.users.models import User
from . import BaseFactory
class UserFactory(BaseFactory):
"""User模型的工厂。"""
class Meta:
model = User
skip_postgeneration_save = True
email = factory.LazyAttribute(
lambda obj: f"{obj.name.lower().replace(' ', '.')}@example.com"
)
name = factory.Faker("name")
is_active = True
@factory.post_generation
def password(obj, create, extracted, **kwargs):
password = extracted or "testpass123"
obj.set_password(password)
if create:
obj.save(update_fields=["password"])
@classmethod
def _create(cls, model_class, *args, **kwargs):
"""显式处理密码的创建方法。"""
password = kwargs.pop("password", None)
user = super()._create(model_class, *args, **kwargs)
if password:
user.set_password(password)
user.save(update_fields=["password"])
return user带关联关系的订单工厂():
factories/order.pypython
import factory
from decimal import Decimal
from apps.orders.models import Order, OrderItem
from . import BaseFactory
from .user import UserFactory
from .product import ProductFactory
class OrderFactory(BaseFactory):
"""Order模型的工厂。"""
class Meta:
model = Order
user = factory.SubFactory(UserFactory)
status = "pending"
total = factory.LazyAttribute(lambda obj: Decimal("0.00"))
@factory.post_generation
def items(obj, create, extracted, **kwargs):
if not create:
return
if extracted:
for item in extracted:
OrderItemFactory(order=obj, **item)
else:
# 创建1-3个随机商品
import random
for _ in range(random.randint(1, 3)):
OrderItemFactory(order=obj)
# 更新总价
obj.total = sum(item.subtotal for item in obj.items.all())
obj.save(update_fields=["total"])
class OrderItemFactory(BaseFactory):
"""OrderItem模型的工厂。"""
class Meta:
model = OrderItem
order = factory.SubFactory(OrderFactory)
product = factory.SubFactory(ProductFactory)
quantity = factory.fuzzy.FuzzyInteger(1, 5)
unit_price = factory.LazyAttribute(lambda obj: obj.product.price)Register Fixtures
注册Fixture
In :
conftest.pypython
from tests.factories.user import UserFactory
from tests.factories.product import ProductFactory
from tests.factories.order import OrderFactory
@pytest.fixture
def user_factory():
return UserFactory
@pytest.fixture
def product_factory():
return ProductFactory
@pytest.fixture
def order_factory():
return OrderFactory在中:
conftest.pypython
from tests.factories.user import UserFactory
from tests.factories.product import ProductFactory
from tests.factories.order import OrderFactory
@pytest.fixture
def user_factory():
return UserFactory
@pytest.fixture
def product_factory():
return ProductFactory
@pytest.fixture
def order_factory():
return OrderFactoryTest Patterns
测试模式
Model Tests
模型测试
python
undefinedpython
undefinedtests/unit/models/test_user.py
tests/unit/models/test_user.py
import pytest
from apps.users.models import User
@pytest.mark.django_db
class TestUserModel:
def test_create_user(self, user_factory):
user = user_factory(email="test@example.com", name="Test User")
assert user.email == "test@example.com"
assert user.name == "Test User"
assert user.is_active is True
assert user.id is not None
def test_soft_delete(self, user_factory):
user = user_factory()
user.delete()
assert user.is_deleted is True
assert User.objects.filter(id=user.id).exists()
def test_display_name(self, user_factory):
user = user_factory(name="John Doe")
assert user.display_name == "John Doe"
user_no_name = user_factory(name="", email="jane@example.com")
assert user_no_name.display_name == "jane"undefinedimport pytest
from apps.users.models import User
@pytest.mark.django_db
class TestUserModel:
def test_create_user(self, user_factory):
user = user_factory(email="test@example.com", name="Test User")
assert user.email == "test@example.com"
assert user.name == "Test User"
assert user.is_active is True
assert user.id is not None
def test_soft_delete(self, user_factory):
user = user_factory()
user.delete()
assert user.is_deleted is True
assert User.objects.filter(id=user.id).exists()
def test_display_name(self, user_factory):
user = user_factory(name="John Doe")
assert user.display_name == "John Doe"
user_no_name = user_factory(name="", email="jane@example.com")
assert user_no_name.display_name == "jane"undefinedService Tests
服务测试
python
undefinedpython
undefinedtests/unit/services/test_user_service.py
tests/unit/services/test_user_service.py
import pytest
from unittest.mock import patch, MagicMock
from apps.users.services import UserService
@pytest.mark.django_db
class TestUserService:
def test_create_user(self, user_factory):
user = UserService.create(
email="new@example.com",
name="New User",
password="securepass123",
)
assert user.email == "new@example.com"
assert user.check_password("securepass123")
def test_create_user_duplicate_email(self, user_factory):
user_factory(email="existing@example.com")
with pytest.raises(ValueError, match="already registered"):
UserService.create(
email="existing@example.com",
name="Another User",
password="password123",
)
@patch("apps.users.services.email.send_welcome_email")
def test_create_user_sends_email(self, mock_send, user_factory):
user = UserService.create(
email="new@example.com",
name="New User",
password="password123",
)
mock_send.assert_called_once_with(user)undefinedimport pytest
from unittest.mock import patch, MagicMock
from apps.users.services import UserService
@pytest.mark.django_db
class TestUserService:
def test_create_user(self, user_factory):
user = UserService.create(
email="new@example.com",
name="New User",
password="securepass123",
)
assert user.email == "new@example.com"
assert user.check_password("securepass123")
def test_create_user_duplicate_email(self, user_factory):
user_factory(email="existing@example.com")
with pytest.raises(ValueError, match="already registered"):
UserService.create(
email="existing@example.com",
name="Another User",
password="password123",
)
@patch("apps.users.services.email.send_welcome_email")
def test_create_user_sends_email(self, mock_send, user_factory):
user = UserService.create(
email="new@example.com",
name="New User",
password="password123",
)
mock_send.assert_called_once_with(user)undefinedAPI Tests
API测试
python
undefinedpython
undefinedtests/integration/api/test_users.py
tests/integration/api/test_users.py
import pytest
@pytest.mark.django_db
class TestUsersAPI:
def test_list_users_requires_auth(self, api_client):
response = api_client.get("/users/")
assert response.status_code == 401
def test_list_users(self, authenticated_client, user_factory):
user_factory.create_batch(5)
response = authenticated_client.get("/users/")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) >= 5
def test_create_user(self, authenticated_client):
response = authenticated_client.post("/users/", json={
"email": "new@example.com",
"name": "New User",
"password": "securepass123",
})
assert response.status_code == 201
assert response.json()["email"] == "new@example.com"
def test_get_user(self, authenticated_client, user_factory):
user = user_factory()
response = authenticated_client.get(f"/users/{user.id}")
assert response.status_code == 200
assert response.json()["id"] == str(user.id)
def test_get_user_not_found(self, authenticated_client):
import uuid
fake_id = uuid.uuid4()
response = authenticated_client.get(f"/users/{fake_id}")
assert response.status_code == 404undefinedimport pytest
@pytest.mark.django_db
class TestUsersAPI:
def test_list_users_requires_auth(self, api_client):
response = api_client.get("/users/")
assert response.status_code == 401
def test_list_users(self, authenticated_client, user_factory):
user_factory.create_batch(5)
response = authenticated_client.get("/users/")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) >= 5
def test_create_user(self, authenticated_client):
response = authenticated_client.post("/users/", json={
"email": "new@example.com",
"name": "New User",
"password": "securepass123",
})
assert response.status_code == 201
assert response.json()["email"] == "new@example.com"
def test_get_user(self, authenticated_client, user_factory):
user = user_factory()
response = authenticated_client.get(f"/users/{user.id}")
assert response.status_code == 200
assert response.json()["id"] == str(user.id)
def test_get_user_not_found(self, authenticated_client):
import uuid
fake_id = uuid.uuid4()
response = authenticated_client.get(f"/users/{fake_id}")
assert response.status_code == 404undefinedFixtures
Fixture进阶
See for advanced fixture patterns including:
references/fixtures.md- Database transactions
- File uploads
- External service mocking
- Time freezing
请查看了解高级Fixture模式,包括:
references/fixtures.md- 数据库事务
- 文件上传
- 外部服务模拟
- 时间冻结
Additional Resources
额外资源
Reference Files
参考文件
- - Advanced fixture patterns, mocking, parametrization
references/fixtures.md
- - 高级Fixture模式、模拟、参数化
references/fixtures.md
Related Skills
相关Skill
- django-dev - Core Django patterns
- django-dev-ninja - API patterns being tested
- django-dev - 核心Django模式
- django-dev-ninja - 待测试的API模式