django-tdd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Django Testing with TDD

基于TDD的Django测试

Test-driven development for Django applications using pytest, factory_boy, and Django REST Framework.
使用pytest、factory_boy和Django REST Framework进行Django应用的测试驱动开发。

When to Activate

适用场景

  • Writing new Django applications
  • Implementing Django REST Framework APIs
  • Testing Django models, views, and serializers
  • Setting up testing infrastructure for Django projects
  • 编写新的Django应用
  • 实现Django REST Framework API
  • 测试Django模型、视图和序列化器
  • 为Django项目搭建测试基础设施

TDD Workflow for Django

Django的TDD工作流

Red-Green-Refactor Cycle

红-绿-重构循环

python
undefined
python
undefined

Step 1: RED - Write failing test

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
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

Step 2: GREEN - Make test pass

Create User model or factory

Create User model or factory

Step 3: REFACTOR - Improve while keeping tests green

Step 3: REFACTOR - Improve while keeping tests green

undefined
undefined

Setup

环境搭建

pytest Configuration

pytest配置

ini
undefined
ini
undefined

pytest.ini

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
undefined
[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
undefined

Test Settings

测试环境配置

python
undefined
python
undefined

config/settings/test.py

config/settings/test.py

from .base import *
DEBUG = True DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', } }
from .base import *
DEBUG = True DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', } }

Disable migrations for speed

Disable migrations for speed

class DisableMigrations: def contains(self, item): return True
def __getitem__(self, item):
    return None
MIGRATION_MODULES = DisableMigrations()
class DisableMigrations: def contains(self, item): return True
def __getitem__(self, item):
    return None
MIGRATION_MODULES = DisableMigrations()

Faster password hashing

Faster password hashing

PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.MD5PasswordHasher', ]
PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.MD5PasswordHasher', ]

Email backend

Email backend

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Celery always eager

Celery always eager

CELERY_TASK_ALWAYS_EAGER = True CELERY_TASK_EAGER_PROPAGATES = True
undefined
CELERY_TASK_ALWAYS_EAGER = True CELERY_TASK_EAGER_PROPAGATES = True
undefined

conftest.py

conftest.py配置

python
undefined
python
undefined

tests/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
undefined
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
undefined

Factory Boy

Factory Boy工具

Factory Setup

工厂配置

python
undefined
python
undefined

tests/factories.py

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): """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)
undefined
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): """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)
undefined

Using Factories

使用工厂

python
undefined
python
undefined

tests/test_models.py

tests/test_models.py

import pytest from 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
undefined
import pytest from 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
undefined

Model Testing

模型测试

Model Tests

模型测试用例

python
undefined
python
undefined

tests/test_models.py

tests/test_models.py

import pytest from django.core.exceptions import ValidationError from tests.factories import UserFactory, ProductFactory
class TestUserModel: """Test User model."""
def test_create_user(self, db):
    """Test creating a regular user."""
    user = UserFactory(email='test@example.com')
    assert user.email == 'test@example.com'
    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='admin@example.com',
        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='test@example.com')
    assert str(user) == 'test@example.com'
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
undefined
import pytest from django.core.exceptions import ValidationError from tests.factories import UserFactory, ProductFactory
class TestUserModel: """Test User model."""
def test_create_user(self, db):
    """Test creating a regular user."""
    user = UserFactory(email='test@example.com')
    assert user.email == 'test@example.com'
    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='admin@example.com',
        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='test@example.com')
    assert str(user) == 'test@example.com'
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
undefined

View Testing

视图测试

Django View Testing

Django视图测试

python
undefined
python
undefined

tests/test_views.py

tests/test_views.py

import pytest from django.urls import reverse from 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()
undefined
import pytest from django.urls import reverse from 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()
undefined

DRF API Testing

DRF API测试

Serializer Testing

序列化器测试

python
undefined
python
undefined

tests/test_serializers.py

tests/test_serializers.py

import pytest from rest_framework.exceptions import ValidationError from apps.products.serializers import ProductSerializer from 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
undefined
import pytest from rest_framework.exceptions import ValidationError from apps.products.serializers import ProductSerializer from 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
undefined

API ViewSet Testing

API视图集测试

python
undefined
python
undefined

tests/test_api.py

tests/test_api.py

import pytest from rest_framework.test import APIClient from rest_framework import status from django.urls import reverse from 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
undefined
import pytest from rest_framework.test import APIClient from rest_framework import status from django.urls import reverse from 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
undefined

Mocking and Patching

Mock与补丁

Mocking External Services

模拟外部服务

python
undefined
python
undefined

tests/test_views.py

tests/test_views.py

from unittest.mock import patch, Mock import 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
undefined
from unittest.mock import patch, Mock import 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
undefined

Mocking Email Sending

模拟邮件发送

python
undefined
python
undefined

tests/test_email.py

tests/test_email.py

from django.core import mail from 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
undefined
from django.core import mail from 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
undefined

Integration Testing

集成测试

Full Flow Testing

全流程测试

python
undefined
python
undefined

tests/test_integration.py

tests/test_integration.py

import pytest from django.urls import reverse from 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': 'test@example.com',
        'password': 'testpass123',
        'password_confirm': 'testpass123',
    })
    assert response.status_code == 302

    # Step 2: Login
    response = client.post(reverse('users:login'), {
        'email': 'test@example.com',
        '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='test@example.com').exists()
undefined
import pytest from django.urls import reverse from 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': 'test@example.com',
        'password': 'testpass123',
        'password_confirm': 'testpass123',
    })
    assert response.status_code == 302

    # Step 2: Login
    response = client.post(reverse('users:login'), {
        'email': 'test@example.com',
        '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='test@example.com').exists()
undefined

Testing Best Practices

测试最佳实践

DO

建议做法

  • Use factories: Instead of manual object creation
  • One assertion per test: Keep tests focused
  • Descriptive test names:
    test_user_cannot_delete_others_post
  • Test edge cases: Empty inputs, None values, boundary conditions
  • Mock external services: Don't depend on external APIs
  • Use fixtures: Eliminate duplication
  • Test permissions: Ensure authorization works
  • Keep tests fast: Use
    --reuse-db
    and
    --nomigrations
  • 使用工厂类:替代手动创建对象
  • 每个测试一个断言:保持测试聚焦
  • 描述性测试名称:例如
    test_user_cannot_delete_others_post
  • 测试边缘情况:空输入、None值、边界条件
  • 模拟外部服务:不依赖外部API
  • 使用fixture:消除重复代码
  • 测试权限控制:确保授权机制正常工作
  • 保持测试快速:使用
    --reuse-db
    --nomigrations
    参数

DON'T

避免做法

  • Don't test Django internals: Trust Django to work
  • Don't test third-party code: Trust libraries to work
  • Don't ignore failing tests: All tests must pass
  • Don't make tests dependent: Tests should run in any order
  • Don't over-mock: Mock only external dependencies
  • Don't test private methods: Test public interface
  • Don't use production database: Always use test database
  • 不测试Django内部逻辑:信任Django的实现
  • 不测试第三方代码:信任依赖库的实现
  • 不忽略失败测试:所有测试必须通过
  • 不创建依赖测试:测试应可按任意顺序运行
  • 不过度Mock:仅Mock外部依赖
  • 不测试私有方法:测试公共接口即可
  • 不使用生产数据库:始终使用测试专用数据库

Coverage

覆盖率统计

Coverage Configuration

覆盖率配置

bash
undefined
bash
undefined

Run tests with coverage

Run tests with coverage

pytest --cov=apps --cov-report=html --cov-report=term-missing
pytest --cov=apps --cov-report=html --cov-report=term-missing

Generate HTML report

Generate HTML report

open htmlcov/index.html
undefined
open htmlcov/index.html
undefined

Coverage Goals

覆盖率目标

ComponentTarget Coverage
Models90%+
Serializers85%+
Views80%+
Services90%+
Utilities80%+
Overall80%+
组件目标覆盖率
模型90%+
序列化器85%+
视图80%+
服务90%+
工具类80%+
整体80%+

Quick Reference

快速参考

PatternUsage
@pytest.mark.django_db
Enable database access
client
Django test client
api_client
DRF API client
factory.create_batch(n)
Create multiple objects
patch('module.function')
Mock external dependencies
override_settings
Temporarily change settings
force_authenticate()
Bypass authentication in tests
assertRedirects
Check for redirects
assertTemplateUsed
Verify template usage
mail.outbox
Check sent emails
Remember: Tests are documentation. Good tests explain how your code should work. Keep them simple, readable, and maintainable.
模式用途
@pytest.mark.django_db
启用数据库访问
client
Django测试客户端
api_client
DRF API测试客户端
factory.create_batch(n)
创建多个对象实例
patch('module.function')
Mock外部依赖
override_settings
临时修改配置
force_authenticate()
跳过认证流程测试
assertRedirects
检查重定向是否正确
assertTemplateUsed
验证模板是否被使用
mail.outbox
检查已发送邮件
记住:测试就是文档。优秀的测试能清晰说明代码的预期行为。保持测试简单、可读且易于维护。