Django应用测试:TestCase/Client/ORM测试

Python 测试与调试专题 · 全面保障Django项目质量

专题: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)