← 返回测试与调试目录
← 返回学习笔记首页
专题: Python 测试与调试系统学习
关键词: Python, 测试, 调试, Django测试, TestCase, TestClient, pytest-django, ORM测试, Web测试
一、Django测试概述
1.1 测试层级与策略
Django是一个功能完备的全栈Web框架,其测试体系覆盖从单元测试到集成测试的完整层级。在Django项目中,测试策略通常遵循"测试金字塔"原则:底层有大量快速的单元测试(模型方法、表单验证、工具函数),中层有适中的集成测试(视图交互、ORM查询、API端点),顶层有少量的端到端测试(用户流程模拟、UI交互)。Django内置的测试框架基于Python标准库的unittest模块进行了深度扩展,提供了针对Web应用场景的专用测试类。测试金字塔的每一层都有其独特的关注点:单元测试关注单个函数或方法的正确性,集成测试关注组件间的协作,而端到端测试则验证完整的用户流程。在实际项目中,推荐将约70%的测试预算分配给单元测试,20%分配给集成测试,10%分配给端到端测试。这种分配策略能够在测试速度和代码覆盖率之间取得良好平衡。
1.2 TestCase类体系
Django提供了四个核心的测试用例基类,它们继承自unittest.TestCase并增加了Django特有的功能。SimpleTestCase是最基础的类,不涉及数据库操作,常用于测试视图函数返回的HTTP响应、表单验证逻辑、URL路由配置等。TestCase继承自TransactionTestCase,是使用最频繁的测试类,它在每次测试前后自动重置数据库状态,确保测试之间的数据隔离。TransactionTestCase在每次测试后执行完整的数据库刷新操作,速度较慢但支持测试事务行为(如atomic块内的回滚)。LiveServerTestCase在测试运行时启动一个真实的开发服务器进程,允许使用Selenium等浏览器自动化工具进行完整的用户界面测试。选择合适的TestCase基类是编写高效测试的第一步:如果测试不涉及数据库,使用SimpleTestCase;如果涉及数据库操作但不是事务逻辑,使用TestCase;如果需要验证事务行为(如并发写入、锁机制),使用TransactionTestCase;如果需要进行真实的浏览器交互测试,使用LiveServerTestCase。
from django.test import SimpleTestCase, TestCase, TransactionTestCase, LiveServerTestCase
# SimpleTestCase - 不涉及数据库
class MathUtilsTest (SimpleTestCase):
def test_addition (self):
self.assertEqual(1 + 1 , 2 )
# TestCase - 涉及数据库,自动重置
class ArticleModelTest (TestCase):
def setUp (self):
self.article = Article.objects.create(
title="测试文章" ,
content="这是一篇测试文章的内容"
)
def test_article_creation (self):
self.assertEqual(Article.objects.count(), 1 )
self.assertEqual(self.article.title, "测试文章" )
1.3 测试数据库管理
Django测试框架自动管理测试数据库的生命周期。当运行测试时,Django会根据settings.py中的DATABASES配置创建一个独立的测试数据库(默认名称为test_加上实际数据库名称)。测试数据库在测试运行前自动创建,运行结束后自动销毁。每个测试用例中的数据库操作都被包裹在一个数据库事务中,测试结束后自动回滚,确保测试之间的数据完全隔离。这种机制意味着开发者无需手动清理数据库中的测试数据。测试数据库支持所有Django ORM功能,包括迁移(migrations)、信号(signals)、聚合查询等。需要注意的是,测试数据库默认使用与生产环境相同的数据库引擎,但在内存数据库(如SQLite的:memory:模式)上运行可以显著提升测试速度。对于大型项目,可以通过配置文件指定使用单独的数据库引擎来运行测试,例如在开发环境中使用PostgreSQL,在测试环境中使用SQLite以加速测试执行。
# settings.py - 配置测试数据库
DATABASES = {
'default' : {
'ENGINE' : 'django.db.backends.postgresql' ,
'NAME' : 'myapp' ,
'USER' : 'postgres' ,
'PASSWORD' : 'secret' ,
'HOST' : 'localhost' ,
'PORT' : '5432' ,
'TEST' : { # 测试数据库配置
'NAME' : 'test_myapp' , # 默认: test_ + NAME
'CHARSET' : 'UTF8' ,
'MIRROR' : 'default' , # 镜像配置
},
}
}
# 使用 --keepdb 选项保留测试数据库(加速重复运行)
# python manage.py test --keepdb
# 使用 --parallel 并行运行测试
# python manage.py test --parallel 4
二、TestClient
2.1 Client基础使用
Django的TestClient是测试视图和HTTP交互的核心工具,它模拟了一个轻量级的HTTP请求/响应周期,无需启动真实的Web服务器。Client对象提供了get()、post()、put()、delete()、patch()、head()、options()和trace()等方法,对应标准的HTTP动词。每个方法返回一个Response对象,包含status_code(状态码)、content(响应内容,字节形式)、json()(解析JSON响应)、headers(响应头)和templates(渲染模板列表)等属性。使用Client时,测试代码可以直接调用视图函数,Django内部构建HttpRequest对象并传递给视图层。这种方式比真实的HTTP请求快得多,因为它跳过了WSGI层和网络I/O。Client默认不包含CSRF检查,这在测试中很方便,如果需要在测试中验证CSRF保护,可以通过在测试用例中启用RequestFactory的CSRF中间件来实现。创建Client实例最简单的方式是在测试类中直接实例化,或者使用Django提供的self.client属性(TestCase默认包含)。
from django.test import TestCase, Client
import json
class BlogViewTest (TestCase):
def setUp (self):
self.client = Client() # 创建Client实例
# 或者直接使用 self.client(TestCase已经包含)
def test_home_page (self):
response = self.client.get('/' )
self.assertEqual(response.status_code, 200 )
self.assertTemplateUsed(response, 'home.html' )
def test_create_article (self):
response = self.client.post('/articles/create/' , {
'title' : '新文章' ,
'content' : '文章内容详情' ,
})
self.assertEqual(response.status_code, 302 ) # 重定向到详情页
self.assertRedirects(response, '/articles/1/' )
def test_json_api (self):
response = self.client.get('/api/articles/' ,
HTTP_ACCEPT='application/json' )
self.assertEqual(response.status_code, 200 )
data = response.json()
self.assertIn('results' , data)
2.2 登录与会话控制
在测试需要用户认证的功能时,TestClient提供了两种主要的登录方式。第一种是使用Client的login()方法,它需要传入用户名和密码,内部会模拟表单提交认证流程并设置session。第二种是使用force_login()方法,它直接设置认证后端中的用户状态,无需密码验证,速度更快。对于需要测试特定session数据的场景,Client提供了session()上下文管理器,可以在其中直接修改session字典。此外,Client还可以保持cookies和session状态,这意味着在同一个测试方法中对Client的多次调用会维持登录状态。这在测试多个需要认证的视图时非常有用,无需在每个测试方法中重复登录。需要注意的是,login()方法会触发完整的认证流程,包括密码哈希验证,而force_login()则完全跳过这一步,适用于对认证流程本身不关心的测试场景。
from django.test import TestCase
from django.contrib.auth.models import User
class AuthViewTest (TestCase):
def setUp (self):
self.user = User.objects.create_user(
username='testuser' ,
password='testpass123' ,
email='test@example.com'
)
def test_login_with_credentials (self):
# 方式一:使用login()模拟真实登录
login_success = self.client.login(
username='testuser' ,
password='testpass123'
)
self.assertTrue(login_success)
def test_force_login (self):
# 方式二:使用force_login()跳过认证流程
self.client.force_login(self.user)
response = self.client.get('/profile/' )
self.assertEqual(response.status_code, 200 )
def test_session_manipulation (self):
# 直接操控session数据
session = self.client.session
session['cart_items' ] = ['item1' , 'item2' ]
session.save()
response = self.client.get('/cart/' )
self.assertContains(response, 'item1' )
2.3 文件上传与Ajax请求
Django TestClient支持模拟文件上传,这在测试包含FileField或ImageField的表单时不可或缺。上传文件时只需使用Python的io.BytesIO或io.StringIO创建类文件对象,并将其作为POST数据的一部分传入。对于Ajax请求的模拟,可以通过在请求头中设置HTTP_X_REQUESTED_WITH='XMLHttpRequest'来标记请求为Ajax类型,这对于测试视图中的request.is_ajax()分支逻辑非常重要。Client还支持自定义HTTP头(以HTTP_为前缀)、设置Cookie(通过cookies参数或者HTTP_COOKIE头)、以及控制follow参数来自动跟随重定向。文件上传测试尤其需要注意文件对象的seek指针位置和编码方式,通常使用BytesIO而非StringIO来确保二进制安全。在实际项目中,还可以创建临时的文件存储目录,通过override_settings(MEDIA_ROOT=temp_dir)来隔离测试产生的媒体文件,避免污染开发环境。
from django.test import TestCase
from django.core.files.uploadedfile import SimpleUploadedFile
import io
class FileUploadTest (TestCase):
def test_file_upload (self):
# 创建模拟文件
file_content = io.BytesIO(b'This is a test file content.' )
uploaded = SimpleUploadedFile(
name='test.txt' ,
content=file_content.read(),
content_type='text/plain'
)
response = self.client.post('/upload/' , {
'file' : uploaded,
'description' : '测试文件描述'
})
self.assertEqual(response.status_code, 200 )
self.assertContains(response, '上传成功' )
def test_ajax_request (self):
# 模拟Ajax请求
response = self.client.get(
'/api/search/' ,
{'q' : 'django' },
HTTP_X_REQUESTED_WITH='XMLHttpRequest' ,
HTTP_ACCEPT='application/json'
)
self.assertEqual(response.status_code, 200 )
data = response.json()
self.assertGreater(len(data['results' ]), 0 )
def test_custom_headers (self):
response = self.client.get('/api/data/' ,
HTTP_AUTHORIZATION='Token abc123' ,
HTTP_USER_AGENT='TestClient/1.0'
)
self.assertEqual(response.status_code, 200 )
三、ORM数据测试
3.1 模型创建与查询测试
ORM测试是Django测试中最基础也最重要的部分,它确保数据模型的创建、查询、更新和删除操作符合预期。编写模型测试时,需要在setUp方法或单独的测试方法中创建模型实例,然后使用Django ORM的查询API验证数据的正确性。Django TestCase确保每个测试方法运行时数据库都是干净的——之前测试产生的数据不会干扰当前测试。这得益于Django将每个测试方法包裹在一个数据库事务中,测试完成后自动回滚。在测试查询时,应覆盖常见的查询场景:精确查询(exact)、模糊查询(icontains)、范围查询(range)、关联查询(select_related/prefetch_related)以及聚合查询(aggregate/annotate)。对于包含ForeignKey或多对多关系的复杂模型,需要创建关联对象并验证跨模型查询的正确性。此外,模型中的自定义方法、属性(property)和Manager方法也应在测试中覆盖。
from django.test import TestCase
from django.utils import timezone
from datetime import timedelta
from .models import Category, Article, Tag, Comment
class ArticleModelTest (TestCase):
def setUp (self):
self.category = Category.objects.create(
name='Python' , slug='python' )
self.tag = Tag.objects.create(name='Django' )
self.article = Article.objects.create(
title='Django ORM详解' ,
content='这是一篇关于Django ORM的详细介绍文章。' ,
category=self.category,
status='published' ,
pub_date=timezone.now()
)
self.article.tags.add(self.tag)
def test_article_creation (self):
# 验证模型创建
self.assertEqual(Article.objects.count(), 1 )
self.assertEqual(self.article.title, 'Django ORM详解' )
self.assertEqual(self.article.category.name, 'Python' )
def test_queryset_filtering (self):
# 测试查询过滤
published = Article.objects.filter(status='published' )
self.assertEqual(published.count(), 1 )
recent = Article.objects.filter(
pub_date__gte=timezone.now() - timedelta(days=7 ))
self.assertEqual(recent.count(), 1 )
def test_article_str_method (self):
# 测试模型 __str__ 方法
self.assertEqual(str(self.article), 'Django ORM详解' )
3.2 数据Fixtures
Django支持通过fixture机制加载预定义的测试数据。Fixture是包含序列化数据的文件,支持JSON、XML和YAML格式。在测试类上使用fixtures属性指定要加载的fixture文件列表后,Django会在每个测试方法执行前自动将这些数据加载到测试数据库中。Fixture适用于需要在多个测试用例中共享基础数据集的场景,例如预设的用户角色、分类目录、配置数据等。然而,过度依赖fixture可能导致测试变得脆弱——fixture中的微小变化可能影响大量测试,而且fixture数据难以追踪和维护。因此,推荐在测试方法中直接创建所需数据(如使用setUp方法),仅在需要大量初始化数据的场景下使用fixture。对于复杂的项目,还可以考虑使用factory_boy等模型工厂库来替代fixture,它提供了更灵活的数据生成方式。管理fixture的标准流程是使用python manage.py dumpdata命令从已有数据库中导出数据,然后根据需要进行裁剪和修改。
# articles/fixtures/test_data.json
[
{
"model" : "articles.article" ,
"pk" : 1,
"fields" : {
"title" : "Django入门教程" ,
"content" : "这是Django入门教程的内容..." ,
"status" : "published" ,
"pub_date" : "2026-05-01T10:00:00Z" ,
"views" : 100
}
},
{
"model" : "articles.category" ,
"pk" : 1,
"fields" : {
"name" : "Python" ,
"slug" : "python"
}
}
]
from django.test import TestCase
class ArticleFixtureTest (TestCase):
# 指定fixture文件(无需扩展名)
fixtures = ['test_data.json' ]
def test_fixture_data_loaded (self):
from .models import Article
self.assertEqual(Article.objects.count(), 1 )
article = Article.objects.get(pk=1 )
self.assertEqual(article.title, 'Django入门教程' )
self.assertEqual(article.views, 100 )
def test_view_counts (self):
from .models import Article
popular = Article.objects.filter(views__gte=50 )
self.assertEqual(popular.count(), 1 )
3.3 factory_boy模型工厂
factory_boy是一个强大的Python库,用于在测试中快速创建模型实例。与Django内置的fixture机制相比,factory_boy提供了更大的灵活性和可维护性。使用factory_boy,开发者可以定义Factory类,指定模型字段的默认值和生成策略,然后在测试中通过Factory类的create()或build()方法快速生成模型实例。create()方法将实例持久化到数据库,而build()方法仅在内存中构建实例,适合不需要数据库持久化的场景。factory_boy支持字段值的惰性求值、序列(Sequence)、子工厂(SubFactory)以及模糊数据生成(Faker集成),使得测试数据的创建既简洁又富有表现力。子工厂特性尤其重要——当模型包含ForeignKey关系时,SubFactory会自动创建关联模型实例,省去了手动创建关联对象的繁琐步骤。对于大型测试套件,factory_boy能显著减少测试代码中的样板代码,提高测试的可读性和可维护性。
# factories.py - 定义模型工厂
import factory
from django.utils import timezone
from .models import Category, Article, Comment, User
class UserFactory (factory.django.DjangoModelFactory):
class Meta :
model = User
username = factory.Sequence(lambda n: f'user{n}' )
email = factory.LazyAttribute(lambda o: f'{o.username}@example.com' )
password = factory.PostGenerationMethodCall('set_password' , 'testpass' )
class CategoryFactory (factory.django.DjangoModelFactory):
class Meta :
model = Category
name = factory.Sequence(lambda n: f'Category {n}' )
slug = factory.Sequence(lambda n: f'category-{n}' )
class ArticleFactory (factory.django.DjangoModelFactory):
class Meta :
model = Article
title = factory.Sequence(lambda n: f'Article Title {n}' )
content = factory.Faker('paragraph' , nb_sentences=5 )
category = factory.SubFactory(CategoryFactory)
author = factory.SubFactory(UserFactory)
status = 'draft'
pub_date = factory.LazyFunction(timezone.now)
# tests.py - 使用factory_boy的测试
from django.test import TestCase
from .factories import ArticleFactory, UserFactory
class ArticleFactoryTest (TestCase):
def test_create_article_with_factory (self):
# 使用工厂创建文章(自动创建关联的分类和用户)
article = ArticleFactory()
self.assertIsNotNone(article.pk)
self.assertIsNotNone(article.category.pk)
self.assertIsNotNone(article.author.pk)
self.assertEqual(Article.objects.count(), 1 )
def test_build_article_without_db (self):
# 仅在内存中构建(不写入数据库)
article = ArticleFactory.build()
self.assertIsNone(article.pk)
self.assertEqual(Article.objects.count(), 0 )
def test_batch_creation (self):
# 批量创建测试数据
articles = ArticleFactory.create_batch(10 , status='published' )
self.assertEqual(Article.objects.filter(status='published' ).count(), 10 )
3.4 数据隔离策略
Django测试框架的核心设计原则之一是测试隔离——每个测试方法不应受到其他测试方法的影响。TestCase类通过在每个测试方法周围包裹数据库事务来实现这一目标。具体来说,Django在setUp()之前开始一个事务,在tearDown()之后回滚该事务,从而确保每次测试都在干净的数据库状态中运行。然而,有些操作(如某些数据库调用的DDL语句)可能会隐式提交事务,破坏测试隔离。在这种情况下,需要使用TransactionTestCase,它在每次测试后执行完整的数据库刷新(而不是简单的回滚),虽然较慢但更加可靠。此外,对于编写自定义数据隔离逻辑的开发者,可以在setUp()和tearDown()中使用django.test.testcases.TestCase._databases变量访问测试数据库设置,或直接使用django.test.utils.setup_databases()和teardown_databases()进行手动管理。
四、视图与URL测试
4.1 视图响应测试
视图测试是Django应用测试的核心环节,它验证视图函数是否正确处理HTTP请求并返回预期的响应。通过TestClient发送HTTP请求后,可以检查响应的状态码、内容、模板、头信息等多个方面。除了检查200 OK状态码外,还应测试各种边缘情况:404页面不存在、403权限不足、400参数错误、500服务器错误等。Django提供了丰富的响应断言方法:assertEqual用于检查状态码,assertTemplateUsed验证使用的模板,assertContains检查响应内容中是否包含特定文本(可设置html=True进行HTML解码匹配),assertJSONEqual比较JSON响应,assertRedirects验证重定向的目标URL。对于类视图(Class-Based Views),测试过程与函数视图类似,但需要额外关注视图的as_view()机制、Mixin行为以及dispatch()方法的路由逻辑。
from django.test import TestCase
from django.urls import reverse
from .factories import ArticleFactory
class ArticleDetailViewTest (TestCase):
def setUp (self):
self.article = ArticleFactory(status='published' )
self.url = reverse('article-detail' , args=[self.article.pk])
def test_detail_view_returns_200 (self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200 )
def test_detail_view_uses_correct_template (self):
response = self.client.get(self.url)
self.assertTemplateUsed(response, 'articles/detail.html' )
def test_detail_view_contains_article_data (self):
response = self.client.get(self.url)
self.assertContains(response, self.article.title)
self.assertContains(response, self.article.content[:50 ])
def test_detail_view_not_found (self):
response = self.client.get(reverse('article-detail' , args=[9999 ]))
self.assertEqual(response.status_code, 404 )
4.2 URL反向解析
Django强烈推荐在测试中使用reverse()函数进行URL反向解析,而不是硬编码URL路径。这种做法将URL配置与测试代码解耦——即使URL模式发生变化,只要视图的命名不改变,测试代码就无需修改。reverse()函数接受视图名称和可选的参数(args)或关键字参数(kwargs),返回对应的URL路径。对于嵌套路由和命名空间(namespace),需要在视图名称前加上命名空间前缀,如'admin:index'或'blog:article_detail'。reverse()还支持查询字符串参数,可以通过urlencode工具手动构建。在复杂的URL配置中,可以使用resolve()函数反向解析URL路径对应的视图函数,这在测试中间件或URL路由配置时特别有用。此外,对于包含参数的URL模式,rever()支持位置参数和关键字参数两种传入方式,前者适用于简单的整型ID,后者适用于包含slug等命名参数的复杂URL。
from django.test import SimpleTestCase
from django.urls import reverse, resolve
from django.utils.http import urlencode
class URLRoutingTest (SimpleTestCase):
def test_article_list_url (self):
url = reverse('article-list' )
self.assertEqual(url, '/articles/' )
def test_article_detail_url (self):
url = reverse('article-detail' , args=[42 ])
self.assertEqual(url, '/articles/42/' )
def test_article_detail_with_slug (self):
url = reverse('article-detail-slug' , kwargs={
'pk' : 42 ,
'slug' : 'django-orm-guide'
})
self.assertEqual(url, '/articles/42/django-orm-guide/' )
def test_search_url_with_query (self):
base = reverse('article-search' )
query = urlencode({'q' : 'django' , 'page' : 2 })
self.assertEqual(f'{base}?{query}' , '/articles/search/?q=django&page=2' )
def test_url_resolves_to_correct_view (self):
# 测试URL解析到正确的视图函数
func = resolve('/articles/' )
self.assertEqual(func.func.__name__, 'ArticleListView' ) # 注意: 类视图实际为view函数
4.3 权限与分页测试
视图测试中需要覆盖权限控制和分页功能这两类常见的业务逻辑。权限测试确保只有具备相应权限的用户才能访问受保护的视图——需要分别测试未认证用户、已认证但无权限用户和已认证且有权限用户三种场景。Django的权限系统包括登录要求(login_required)、权限要求(permission_required)和自定义权限检查。分页测试则验证Paginator类的行为是否正确:需要测试每页数量、总页数计算、超出范围的页面处理、以及分页导航中页码的正确渲染。对于类视图,可以通过测试混入类(Mixin)的行为来验证权限和分页逻辑:例如测试LoginRequiredMixin在未登录时是否重定向到登录页面,或测试PaginatorMixin在page参数超出范围时是否正确返回404。此外,对于ListView类型的视图,Django的MultipleObjectMixin自动处理了分页逻辑,可以通过检查响应上下文中的page_obj和paginator对象来验证分页是否正常工作。
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User, Permission
from .factories import ArticleFactory
class PermissionViewTest (TestCase):
def setUp (self):
self.user = User.objects.create_user('user' , password='pass' )
self.admin = User.objects.create_user('admin' , password='pass' )
# 给admin用户添加文章发布权限
perm = Permission.objects.get(codename='can_publish_article' )
self.admin.user_permissions.add(perm)
def test_unauthenticated_user_redirected (self):
response = self.client.get(reverse('article-create' ))
self.assertEqual(response.status_code, 302 )
self.assertIn('/login/' , response.url)
def test_user_without_permission_gets_403 (self):
self.client.force_login(self.user)
response = self.client.get(reverse('article-create' ))
self.assertEqual(response.status_code, 403 )
class PaginationViewTest (TestCase):
def setUp (self):
# 创建25篇文章(分页每页10篇)
ArticleFactory.create_batch(25 , status='published' )
self.url = reverse('article-list' )
def test_first_page_returns_10_articles (self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200 )
self.assertEqual(len(response.context['object_list' ]), 10 )
def test_second_page_returns_10_articles (self):
response = self.client.get(self.url, {'page' : 2 })
self.assertEqual(response.status_code, 200 )
self.assertEqual(len(response.context['object_list' ]), 10 )
def test_last_page_returns_remaining_articles (self):
response = self.client.get(self.url, {'page' : 3 })
self.assertEqual(response.status_code, 200 )
self.assertEqual(len(response.context['object_list' ]), 5 )
def test_invalid_page_returns_404 (self):
response = self.client.get(self.url, {'page' : 100 })
self.assertEqual(response.status_code, 404 )
五、表单测试
5.1 表单验证测试
表单是Django应用中处理用户输入的核心组件,表单测试确保数据验证逻辑的正确性和完整性。Django表单系统提供了一套声明式的字段验证机制,包括内置验证器(如EmailValidator、MinLengthValidator等)和自定义验证方法(以clean_开头的字段级验证和clean()方法的表单级验证)。测试表单验证时,需要覆盖三类场景:有效数据应通过验证、无效数据应产生正确的错误信息、边界条件应被正确处理。对于表单的is_valid()方法调用后的cleaned_data,应验证其包含所有经过验证和清洗的字段值。对于ModelForm,还需验证模型实例是否在表单保存后被正确更新到数据库。此外,表单测试还包括对表单字段的widget渲染、帮助文本(help_text)、标签(label)以及错误信息的显示样式的验证。
from django.test import TestCase
from .forms import ArticleForm, ContactForm
class ArticleFormTest (TestCase):
def test_valid_form (self):
form_data = {
'title' : '有效的文章标题' ,
'content' : '这是一篇长度足够的文章内容。' * 5 ,
'status' : 'draft' ,
}
form = ArticleForm(data=form_data)
self.assertTrue(form.is_valid())
def test_form_required_fields (self):
form = ArticleForm(data={})
self.assertFalse(form.is_valid())
self.assertIn('title' , form.errors)
self.assertIn('content' , form.errors)
def test_title_too_short (self):
form_data = {'title' : 'AB' , 'content' : '有效内容' }
form = ArticleForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('title' , form.errors)
self.assertIn('至少5个字符' , form.errors['title' ])
def test_cleaned_data (self):
form = ArticleForm(data={
'title' : ' 带空格标题 ' ,
'content' : '有效内容' * 5 ,
'status' : 'draft' ,
})
self.assertTrue(form.is_valid())
# 验证数据清洗(去除首尾空格)
self.assertEqual(form.cleaned_data['title' ], '带空格标题' )
5.2 ModelForm与CSRF测试
ModelForm测试不仅需要验证表单字段,还需验证表单与模型的交互行为。在测试ModelForm时,应验证save()方法是否正确创建或更新模型实例,commit=False参数是否按预期工作,以及ManyToManyField等特殊字段在表单处理过程中是否正确保存。对于表单集(formsets)的测试,需要额外验证表单集的管理字段(management form)是否正确初始化、表单的添加和删除操作是否符合预期,以及表单集级别的验证逻辑。CSRF(跨站请求伪造)测试在Django中默认是禁用的(因为TestClient默认不检查CSRF token),但可以通过在请求中设置CSRF token或创建CsrfViewMiddleware启用的测试环境来验证CSRF保护功能。使用RequestFactory替代Client可以更精细地控制请求对象,并手动添加CSRF中间件来测试CSRF保护的端到端流程。
from django.test import TestCase
from .forms import ArticleModelForm
from .models import Article
class ArticleModelFormTest (TestCase):
def test_modelform_creates_article (self):
form = ArticleModelForm(data={
'title' : 'ModelForm创建的文章' ,
'content' : '通过ModelForm创建的内容' ,
'status' : 'published' ,
})
self.assertTrue(form.is_valid())
article = form.save()
self.assertIsNotNone(article.pk)
self.assertEqual(Article.objects.count(), 1 )
self.assertEqual(article.title, 'ModelForm创建的文章' )
def test_modelform_commit_false (self):
form = ArticleModelForm(data={
'title' : '不提交的文章' ,
'content' : '内容' ,
'status' : 'draft' ,
})
self.assertTrue(form.is_valid())
article = form.save(commit=False )
# commit=False不会保存到数据库
self.assertIsNone(article.pk)
# 手动保存
article.save()
self.assertIsNotNone(article.pk)
# CSRF测试示例
from django.test import TestCase, RequestFactory, Client
from django.middleware.csrf import get_token
from django.views.decorators.csrf import csrf_exempt
class CSRFProtectionTest (TestCase):
def test_csrf_token_in_form (self):
# GET请求应包含CSRF token
response = self.client.get('/articles/create/' )
self.assertContains(response, 'csrfmiddlewaretoken' )
def test_post_without_csrf_token_fails (self):
# 不带CSRF token的POST请求应被拒绝
# 注意:Client默认不检查CSRF,此处需要启用
client = Client(enforce_csrf_checks=True )
response = client.post('/articles/create/' , {
'title' : '测试' ,
'content' : '内容' ,
})
self.assertEqual(response.status_code, 403 )
六、认证与权限测试
6.1 用户登录/登出测试
认证测试确保用户登录、登出和会话管理功能正确运作。Django的认证系统包含用户模型(User)、认证后端(Authentication Backends)、登录视图、登出视图以及密码重置流程。在测试登录功能时,需要验证正确凭据能成功登录、错误凭据被拒绝、登录后用户被重定向到正确页面、以及session在登录后包含了认证信息。登出测试则验证登出后session被清除、用户被重定向、以及受保护页面不再可访问。密码重置流程包括请求重置邮件、访问重置链接和设置新密码三个步骤,每个步骤都需要独立测试。此外,Django的认证系统还支持"记住我"功能(通过SESSION_EXPIRE_AT_BROWSER_CLOSE设置)、不同认证后端的组合使用以及自定义用户模型(AbstractUser/AbstractBaseUser的扩展),这些特性应在测试中有所覆盖。
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
User = get_user_model()
class AuthenticationTest (TestCase):
def setUp (self):
self.user = User.objects.create_user(
username='testuser' ,
email='test@example.com' ,
password='correctpassword'
)
self.login_url = reverse('login' )
self.profile_url = reverse('profile' )
def test_successful_login (self):
response = self.client.post(self.login_url, {
'username' : 'testuser' ,
'password' : 'correctpassword' ,
})
# 登录成功后应重定向
self.assertEqual(response.status_code, 302 )
# 验证session中包含认证信息
self.assertTrue(self.client.session.get('_auth_user_id' ))
def test_failed_login_wrong_password (self):
response = self.client.post(self.login_url, {
'username' : 'testuser' ,
'password' : 'wrongpassword' ,
})
self.assertEqual(response.status_code, 200 ) # 重新显示登录页
self.assertContains(response, '请输入正确的用户名和密码' )
def test_logout_clears_session (self):
# 先登录
self.client.login(username='testuser' , password='correctpassword' )
self.assertTrue(self.client.session.get('_auth_user_id' ))
# 执行登出
self.client.get(reverse('logout' ))
# 验证session已清除
# 登出后访问受保护页面应重定向到登录页
response = self.client.get(self.profile_url)
self.assertEqual(response.status_code, 302 )
self.assertIn('/login/' , response.url)
6.2 权限装饰器与Mixin测试
Django提供了多种方式来实现权限控制,常用的包括函数视图中的@login_required和@permission_required装饰器,以及类视图中的LoginRequiredMixin和PermissionRequiredMixin。测试这些权限控制机制时,需要系统性地覆盖不同用户角色的访问场景。对于@login_required装饰的视图,未认证用户应被重定向到登录页面(可设置redirect_field_name自定义重定向参数),认证用户应正常访问。对于@permission_required装饰的视图,需要测试无权限用户返回403、有权限用户正常访问、以及superuser自动拥有所有权限的行为。对于类视图中的Mixin,可以通过创建继承自该Mixin的测试用视图来隔离测试Mixin的行为,或者在完整的视图测试中验证Mixin的效果。此外,还需要测试自定义权限(在模型Meta类中使用permissions定义的权限)以及Django内置的add/change/delete/view四种标准权限。
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User, Permission, Group
from django.contrib.contenttypes.models import ContentType
from .models import Article
class PermissionDecoratorTest (TestCase):
def setUp (self):
self.user = User.objects.create_user(
username='editor' , password='pass' )
self.admin = User.objects.create_superuser(
username='admin' , password='pass' , email='admin@test.com' )
# 给editor用户添加change_article权限
content_type = ContentType.objects.get_for_model(Article)
change_perm = Permission.objects.get(
content_type=content_type, codename='change_article' )
self.user.user_permissions.add(change_perm)
self.edit_url = reverse('article-edit' , args=[1 ])
def test_user_without_permission_gets_403 (self):
# 创建一个没有权限的用户
no_perm_user = User.objects.create_user(
username='viewer' , password='pass' )
self.client.force_login(no_perm_user)
response = self.client.get(self.edit_url)
self.assertEqual(response.status_code, 403 )
def test_user_with_permission_can_access (self):
self.client.force_login(self.user)
response = self.client.get(self.edit_url)
self.assertEqual(response.status_code, 200 )
def test_superuser_has_all_permissions (self):
self.client.force_login(self.admin)
response = self.client.get(self.edit_url)
self.assertEqual(response.status_code, 200 )
6.3 Group权限测试
在实际项目中,权限管理通常通过Group来实现——将用户添加到组,组关联一组权限,从而简化权限分配。测试Group权限需要创建Group对象、添加权限、将用户添加到组,然后验证该用户是否获得了相应的访问权限。Group测试还应覆盖用户从组中移除后权限消失、用户同时属于多个组时权限的合并效果、以及组权限与用户直接权限的交互关系。此外,Django的权限系统是ORM级别的,这意味着权限检查会触发数据库查询,在编写大量权限相关的测试时应注意N+1查询问题,可以使用select_related('user_permissions')或prefetch_related('groups__permissions')来优化。
class GroupPermissionTest (TestCase):
def setUp (self):
# 创建组并分配权限
self.editor_group = Group.objects.create(name='Editors' )
content_type = ContentType.objects.get_for_model(Article)
perms = Permission.objects.filter(
content_type=content_type,
codename__in=['add_article' , 'change_article' , 'view_article' ]
)
self.editor_group.permissions.set(perms)
self.user = User.objects.create_user(
username='group_editor' , password='pass' )
def test_group_permission_granted (self):
# 将用户加入组
self.user.groups.add(self.editor_group)
# 验证权限已获得
self.assertTrue(
self.user.has_perm('articles.add_article' ))
self.assertTrue(
self.user.has_perm('articles.change_article' ))
def test_group_permission_revoked_on_removal (self):
self.user.groups.add(self.editor_group)
self.user.groups.remove(self.editor_group)
# 从组中移除后权限消失
self.assertFalse(
self.user.has_perm('articles.add_article' ))
七、DRF API测试
7.1 APIRequestFactory与APITestCase
Django REST Framework(DRF)提供了专门的测试工具来简化API端点测试。APIRequestFactory是DRF替代Django标准RequestFactory的工具,它生成的是DRF的Request对象(而非Django的HttpRequest),能够正确处理DRF的特性,如内容协商(Content Negotiation)、解析器(Parsers)、认证(Authentication)和限流(Throttling)。APITestCase继承自Django的TestCase,同时混入了DRF的测试辅助方法,包括self.client(APIClient实例)、assertEqual等增强的断言。APIClient是DRF提供的增强版HTTP测试客户端,支持更丰富的API测试功能,如自动内容类型处理(在POST/PUT请求中自动设置Content-Type头)、可配置的默认格式(如JSON)、以及方便的登录/认证方法。使用APIClient时,响应数据可以通过response.data直接访问序列化后的数据,无需手动调用json.loads()。
from rest_framework.test import APITestCase, APIRequestFactory
from rest_framework import status
from django.urls import reverse
from .factories import ArticleFactory
from .models import Article
class ArticleAPITest (APITestCase):
def setUp (self):
# 使用APIRequestFactory
self.factory = APIRequestFactory()
self.article = ArticleFactory(title='API测试文章' )
self.list_url = reverse('article-list' )
self.detail_url = reverse('article-detail' , args=[self.article.pk])
def test_get_article_list (self):
# 使用self.client(APIClient)
response = self.client.get(self.list_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results' ]), 1 )
def test_get_article_detail (self):
response = self.client.get(self.detail_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title' ], 'API测试文章' )
def test_create_article (self):
data = {
'title' : '新创建的文章' ,
'content' : '新文章的内容详情' ,
'status' : 'draft' ,
}
response = self.client.post(self.list_url, data, format='json' )
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Article.objects.count(), 2 )
7.2 认证类与序列化器测试
DRF API测试中,认证测试和序列化器测试是两个关键环节。认证测试需要验证各种认证类的行为:TokenAuthentication测试token的创建、验证和过期处理;SessionAuthentication测试基于session的API访问;BasicAuthentication测试基本HTTP认证;JWTAuthentication测试JSON Web Token的签发、刷新和验证。序列化器测试则验证数据序列化和反序列化的正确性:需要测试序列化器输出中的字段、验证反序列化时的数据校验逻辑、以及嵌套序列化器和自定义字段的行为。对于序列化器的测试,可以通过创建序列化器实例并传入模型实例或原始数据,然后断言输出数据或验证错误信息。对于自定义序列化器字段(如Base64ImageField、JSONField等),需要单独编写单元测试覆盖其转换和验证逻辑。DRF还提供了SerializerTestCaseMixin等辅助类,但大多数情况下直接使用APITestCase即可满足需求。
from rest_framework.test import APITestCase
from rest_framework import status
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
from django.urls import reverse
from .serializers import ArticleSerializer
from .factories import ArticleFactory
class TokenAuthTest (APITestCase):
def setUp (self):
self.user = User.objects.create_user(
username='apiuser' , password='apipass' )
self.token = Token.objects.create(user=self.user)
self.url = reverse('protected-endpoint' )
def test_access_without_token (self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_access_with_token (self):
# 使用token认证
self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}' )
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_access_with_invalid_token (self):
self.client.credentials(HTTP_AUTHORIZATION='Token invalidtoken123' )
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
# 序列化器测试
class ArticleSerializerTest (APITestCase):
def setUp (self):
self.article = ArticleFactory(title='序列化器测试' )
self.serializer = ArticleSerializer(instance=self.article)
def test_serializer_contains_expected_fields (self):
data = self.serializer.data
self.assertIn('id' , data)
self.assertIn('title' , data)
self.assertIn('content' , data)
self.assertIn('created_at' , data)
def test_serializer_read_only_fields (self):
data = self.serializer.data
# 验证只读字段不被deserialization影响
self.assertIsNotNone(data['created_at' ])
def test_serializer_validation (self):
serializer = ArticleSerializer(data={
'title' : '' , # 空标题应验证失败
'content' : '内容' ,
})
self.assertFalse(serializer.is_valid())
self.assertIn('title' , serializer.errors)
八、pytest-django
8.1 配置与基本使用
pytest-django是pytest生态系统中用于Django测试的插件,它提供了比Django内置测试运行器更简洁、更灵活的测试体验。安装pytest和pytest-django后,需要在项目根目录创建pytest.ini或pyproject.toml文件来配置Django设置模块。pytest-django的核心优势包括:更简洁的函数式测试(无需继承TestCase)、强大的fixture系统(自动管理和清理资源)、参数化测试(parametrize装饰器轻松测试多组输入)、以及更好的断言错误信息。在pytest-django中,测试可以是简单的函数而非类方法,这使得测试结构更加扁平化和直观。pytest的conftest.py文件支持跨文件的fixture共享,适合定义在整个测试套件中通用的测试数据生成逻辑。pytest-django还支持自动发现测试文件(匹配test_*.py或*_test.py模式)和测试函数(匹配test_前缀),无需手动组织测试套件。
# pytest.ini 配置
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings.test
python_files = tests.py test_*.py *_tests.py
testpaths = apps/
addopts = --reuse-db --nomigrations --strict-markers -v
# 或使用 pyproject.toml
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "myproject.settings.test"
python_files = ["tests.py" , "test_*.py" , "*_tests.py" ]
testpaths = ["apps/" ]
addopts = "--reuse-db --nomigrations --strict-markers -v"
# conftest.py - 跨文件的共享fixture
import pytest
from django.contrib.auth.models import User
from .factories import ArticleFactory, UserFactory
@pytest.fixture
def api_client ():
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture
def authenticated_client (api_client, django_user_model):
user = django_user_model.objects.create_user(
username='testuser' , password='testpass' )
api_client.force_authenticate(user=user)
return api_client
@pytest.fixture
def article ():
return ArticleFactory()
# test_views.py - 函数式测试
def test_article_list_returns_200 (api_client, article):
from django.urls import reverse
url = reverse('article-list' )
response = api_client.get(url)
assert response.status_code == 200
def test_authenticated_user_can_create (authenticated_client):
from django.urls import reverse
url = reverse('article-list' )
data = {'title' : 'pytest创建的文章' , 'content' : '内容' }
response = authenticated_client.post(url, data)
assert response.status_code == 201
8.2 django_db标记与fixture管理
pytest-django的一个重要特性是django_db标记——默认情况下,pytest-django中的测试函数不允许访问数据库(以提高测试速度),只有明确使用@pytest.mark.django_db装饰器的测试才会获得数据库访问权限。这种设计鼓励开发者将数据库测试和非数据库测试清晰分离。对于需要数据库的测试,django_db标记还支持transaction=True参数,创建真实的事务边界(而不是在测试结束时报错),适用于测试信号、事务原子性等场景。pytest-django的fixture管理系统与Django测试框架相比更加灵活:fixture可以自动清理(通过yield语句)、可以参数化、可以使用其他fixture作为依赖、并且可以在conftest.py中跨文件共享。常用的内置fixture包括rf(RequestFactory)、client(Django TestClient)、admin_client(已认证管理员的Client)和settings(临时覆盖Django设置)。settings fixture尤其重要——它允许在测试中临时修改Django配置,且修改不会影响其他测试。
import pytest
from django.urls import reverse
# 不使用数据库的测试——更快
def test_url_pattern ():
assert reverse('home' ) == '/'
# 使用数据库的测试——需要django_db标记
@pytest.mark.django_db
def test_article_creation ():
from .models import Article
Article.objects.create(title='测试' , content='内容' )
assert Article.objects.count() == 1
# 使用transaction=True测试事务行为
@pytest.mark.django_db (transaction=True )
def test_transaction_rollback ():
from django.db import transaction
from .models import Article
with transaction.atomic():
Article.objects.create(title='事务测试' , content='内容' )
raise Exception('触发回滚' )
# 事务回滚后,数据不应存在
assert Article.objects.count() == 0
# 参数化测试——测试多组输入
@pytest.mark.parametrize ('title,expected' , [
('Valid Title' , True ),
('A' , False ), # 太短
('' , False ), # 空字符串
('x' * 201 , False ), # 超过最大长度
])
@pytest.mark.django_db
def test_article_title_validation (title, expected):
from .forms import ArticleForm
form = ArticleForm(data={'title' : title, 'content' : '有效内容' })
assert form.is_valid() == expected
8.3 settings覆盖与并发测试
pytest-django的settings fixture提供了一种临时覆盖Django配置的机制,在测试函数内修改设置后,测试结束时会自动恢复到原始值。这对于测试特定配置条件下的代码行为非常有用,例如测试不同的缓存后端、不同的文件存储配置、不同的邮件后端、以及不同的DEBUG模式。对于多个测试需要相同配置覆盖的情况,可以将settings fixture与自定义fixture组合使用。并发测试是pytest-django的另一个强大特性——通过pytest-xdist插件,可以将测试分布在多个CPU核心上并行运行。Django测试的并发运行比普通Python测试更加复杂,因为测试数据库的隔离性需要特别处理。pytest-django通过--reuse-db(复用测试数据库)和--create-db(重新创建)选项来管理测试数据库的生命周期。在并行测试中,每个worker进程使用独立的测试数据库(通过数据库名称后缀区分),确保测试之间的完全隔离。并发测试可以显著减少大型测试套件的运行时间,但在使用时应确保测试之间没有隐藏的依赖关系。
# settings fixture 示例 - 临时覆盖Django配置
import pytest
from django.core import mail
@pytest.mark.django_db
def test_email_sent_in_debug_mode (settings):
# 临时开启DEBUG模式
settings.DEBUG = True
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
# 执行发送邮件的操作
from django.core.mail import send_mail
send_mail(
'测试主题' ,
'测试正文' ,
'from@example.com' ,
['to@example.com' ],
)
# 验证邮件已发送
assert len(mail.outbox) == 1
assert mail.outbox[0 ].subject == '测试主题'
@pytest.mark.django_db
def test_custom_storage_backend (settings, tmpdir):
# 临时使用本地文件系统存储
settings.DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
settings.MEDIA_ROOT = str(tmpdir.mkdir('media' ))
# 执行文件上传测试...
pass
# 并发测试配置与使用
# 安装: pip install pytest-xdist pytest-django
# 运行: pytest -n 4 (4个worker并行)
# 运行: pytest -n auto (自动检测CPU核心数)
# 运行: pytest -n 4 --reuse-db (复用测试数据库)
# 标记串行执行的测试(不宜并行的测试)
@pytest.mark.serial
@pytest.mark.django_db
def test_database_migration ():
# 此测试需要串行执行
pass
# pytest.ini 配置并行执行的默认参数
# [pytest]
# addopts = -n auto --dist loadscope
# --dist loadscope: 按测试模块分配,同一模块的测试在同一个worker中执行
# --dist loadfile: 按测试文件分配
# --dist worksteal: 动态负载均衡(pytest-xdist 3.x+)
九、实战案例
9.1 博客系统全栈测试
以下是一个完整的博客系统测试套件示例,涵盖从模型到视图的完整测试链路。博客系统的核心功能包括文章CRUD、评论系统、分类筛选、标签管理、搜索功能和RSS订阅。在编写博客系统的测试时,采用分层测试策略:模型层测试确保数据库约束和自定义方法正确,视图层测试验证HTTP响应和模板渲染,表单层测试检查输入验证逻辑。测试覆盖正常路径和异常路径——用户创造文章、查看文章详情、发表评论、搜索文章等正常流程,以及未登录用户访问受限资源、提交无效表单、访问不存在的页面等异常场景。在实际项目中,一个良好的博客系统测试套件通常包含100个以上的测试用例,覆盖所有核心功能和边界条件。测试代码应遵循DRY原则,通过setUp方法、factory_boy和自定义辅助方法来减少重复代码。以下示例展示了博客系统核心功能的测试模式。
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from .factories import ArticleFactory, CommentFactory, CategoryFactory
from .models import Article, Comment
class BlogFullStackTest (TestCase):
def setUp (self):
# 初始化测试数据
self.user = User.objects.create_user('author' , password='pass' )
self.category = CategoryFactory()
self.article = ArticleFactory(
author=self.user, category=self.category, status='published' )
self.comment = CommentFactory(article=self.article, author=self.user)
def test_full_article_lifecycle (self):
# 1. 创建文章(表单测试)
self.client.force_login(self.user)
response = self.client.post(reverse('article-create' ), {
'title' : '生命周期测试文章' ,
'content' : '生命周期测试内容' ,
'category' : self.category.pk,
'status' : 'draft' ,
})
self.assertEqual(response.status_code, 302 )
new_article = Article.objects.get(title='生命周期测试文章' )
# 2. 查看文章详情
response = self.client.get(reverse('article-detail' , args=[new_article.pk]))
self.assertEqual(response.status_code, 200 )
self.assertContains(response, new_article.title)
# 3. 更新文章
response = self.client.post(reverse('article-edit' , args=[new_article.pk]), {
'title' : '更新后的标题' ,
'content' : new_article.content,
'category' : self.category.pk,
'status' : 'published' ,
})
self.assertEqual(response.status_code, 302 )
updated = Article.objects.get(pk=new_article.pk)
self.assertEqual(updated.title, '更新后的标题' )
# 4. 查看文章出现在列表中
response = self.client.get(reverse('article-list' ))
self.assertContains(response, updated.title)
# 5. 删除文章
response = self.client.post(reverse('article-delete' , args=[updated.pk]))
self.assertEqual(response.status_code, 302 )
self.assertEqual(Article.objects.filter(pk=updated.pk).count(), 0 )
# 博客系统评论与搜索测试
class BlogCommentSearchTest (TestCase):
def setUp (self):
self.user = User.objects.create_user('user1' , password='pass' )
self.article = ArticleFactory(status='published' )
def test_post_comment_as_anonymous (self):
# 匿名用户不能发表评论
response = self.client.post(
reverse('comment-create' , args=[self.article.pk]),
{'content' : '匿名评论' }
)
self.assertEqual(response.status_code, 302 ) # 重定向到登录页
def test_post_comment_as_authenticated (self):
self.client.force_login(self.user)
response = self.client.post(
reverse('comment-create' , args=[self.article.pk]),
{'content' : '这是一条测试评论' }
)
self.assertEqual(response.status_code, 302 )
self.assertEqual(Comment.objects.count(), 1 )
def test_search_articles (self):
# 创建一些文章用于搜索
ArticleFactory(title='Django ORM Guide' , status='published' )
ArticleFactory(title='Python Best Practices' , status='published' )
ArticleFactory(title='REST API Design' , status='published' )
# 搜索Django关键词
response = self.client.get(reverse('article-search' ), {'q' : 'Django' })
self.assertContains(response, 'Django ORM Guide' )
self.assertNotContains(response, 'Python Best Practices' )
def test_empty_search (self):
response = self.client.get(reverse('article-search' ), {'q' : '不存在的关键词' })
self.assertEqual(response.status_code, 200 )
self.assertContains(response, '没有找到相关文章' )
9.2 电商API测试套件
电子商务API是DRF应用的典型场景,测试套件需要覆盖商品管理、购物车、订单处理、支付集成和库存管理等核心功能。以下示例展示了一个电商API测试套件的核心模式。在电商系统的API测试中,权限测试尤为重要——需要验证不同角色(普通用户、商家、管理员)对不同API端点的访问权限。商品API测试包括商品列表的筛选和分页、商品详情的完整信息、商品库存的实时查询。购物车API测试需要验证商品的添加、数量修改、删除以及整个购物车的清空操作。订单API测试覆盖订单创建(从购物车生成)、订单状态流转(待支付、已支付、已发货、已完成、已取消)、订单支付回调处理。库存管理测试需要验证下单时库存扣减、取消订单时库存返还、以及超卖预防机制。在测试支付集成时,通常使用mock模拟第三方支付网关的响应,而不是实际调用外部API。整个电商API测试套件应包含足够的测试用例,确保核心商业逻辑的正确性和数据一致性。
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from django.contrib.auth.models import User
from .factories import ProductFactory, CartFactory, OrderFactory
from .models import Product, Cart, Order, OrderItem
class EcommerceAPITest (APITestCase):
def setUp (self):
# 创建用户和商品
self.user = User.objects.create_user('buyer' , password='pass' )
self.admin = User.objects.create_superuser('admin' , password='pass' , email='a@t.com' )
self.product = ProductFactory(price=99.99 , stock=10 )
self.client.force_authenticate(user=self.user)
def test_product_list_pagination (self):
ProductFactory.create_batch(25 )
response = self.client.get(reverse('product-list' ))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results' ]), 20 ) # 默认每页20条
self.assertIsNotNone(response.data['next' ]) # 有下一页
def test_add_to_cart (self):
response = self.client.post(reverse('cart-add' ), {
'product_id' : self.product.pk,
'quantity' : 2 ,
})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Cart.objects.count(), 1 )
self.assertEqual(Cart.objects.first().quantity, 2 )
def test_add_to_cart_exceeds_stock (self):
# 尝试购买超过库存数量的商品
response = self.client.post(reverse('cart-add' ), {
'product_id' : self.product.pk,
'quantity' : 20 , # 库存只有10
})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_order_from_cart (self):
# 先添加商品到购物车
self.client.post(reverse('cart-add' ), {
'product_id' : self.product.pk,
'quantity' : 1 ,
})
# 从购物车创建订单
response = self.client.post(reverse('order-create' ))
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Order.objects.count(), 1 )
self.assertEqual(OrderItem.objects.count(), 1 )
# 验证库存扣减
self.product.refresh_from_db()
self.assertEqual(self.product.stock, 9 ) # 10 - 1
# 验证购物车已清空
self.assertEqual(Cart.objects.count(), 0 )