进阶测试主题¶
请求工厂¶
RequestFactory
与测试客户端共享相同的 API。 但是,RequestFactory 不能像浏览器那样运行,而是提供一种生成请求实例的方法,该实例可用作任何视图的第一个参数。 这意味着您可以像测试任何其他功能一样测试视图函数——就像一个黑匣子一样,具有确切已知的输入,可以测试特定的输出。
RequestFactory
的 API 是测试客户端 API 的一个稍加限制的子集。
它只能访问 HTTP 的
get()
、post()
、put()
、delete()
、head()
、options()
和trace()
方法。这些方法接受所有相同的参数,除了
follow
。因为这只是一个产生请求的工厂,所以由你来处理响应。它不支持中间件。如果需要视图正常运行,会话和认证属性必须由测试本身提供。
The query_params
parameter was added.
例如¶
下面是一个使用请求工厂的单元测试:
from django.contrib.auth.models import AnonymousUser, User
from django.test import RequestFactory, TestCase
from .views import MyView, my_view
class SimpleTest(TestCase):
def setUp(self):
# Every test needs access to the request factory.
self.factory = RequestFactory()
self.user = User.objects.create_user(
username="jacob", email="jacob@…", password="top_secret"
)
def test_details(self):
# Create an instance of a GET request.
request = self.factory.get("/customer/details")
# Recall that middleware are not supported. You can simulate a
# logged-in user by setting request.user manually.
request.user = self.user
# Or you can simulate an anonymous user by setting request.user to
# an AnonymousUser instance.
request.user = AnonymousUser()
# Test my_view() as if it were deployed at /customer/details
response = my_view(request)
# Use this syntax for class-based views.
response = MyView.as_view()(request)
self.assertEqual(response.status_code, 200)
AsyncRequestFactory¶
RequestFactory
创建 WSGI 类的请求。如果你想创建 ASGI 类的请求,包括有一个正确的 ASGI scope
,你可以使用 django.test.AsyncRequestFactory
。
该类与 RequestFactory
直接 API 兼容,唯一的区别是它返回 ASGIRequest
实例,而不是 WSGIRequest
实例。它的所有方法仍然是可同步调用的。
在 defaults
中的任意关键字参数被直接添加到 ASGI 范围中。
The query_params
parameter was added.
测试基于类的视图¶
为了在请求/响应周期之外测试基于类的视图,你必须确保它们配置正确,在实例化之后调用 setup()
。
例如,假设基于类的视图如下:
from django.views.generic import TemplateView
class HomeView(TemplateView):
template_name = "myapp/home.html"
def get_context_data(self, **kwargs):
kwargs["environment"] = "Production"
return super().get_context_data(**kwargs)
你可以直接测试 get_context_data()
方法,首先实例化视图,然后向 setup()
传递一个 request
,然后再进行测试代码。
from django.test import RequestFactory, TestCase
from .views import HomeView
class HomePageTest(TestCase):
def test_environment_set_in_context(self):
request = RequestFactory().get("/")
view = HomeView()
view.setup(request)
context = view.get_context_data()
self.assertIn("environment", context)
测试与多主机名¶
ALLOWED_HOSTS
配置在运行测试时被验证。这允许测试客户端区分内部和外部 URL。
支持多租户或根据请求的主机改变业务逻辑的项目,以及在测试中使用自定义主机名的项目,必须在 ALLOWED_HOSTS
中包含这些主机。
第一个选项是将主机添加到你的配置文件中。例如,docs.djangoproject.com 的测试套件包括以下内容:
from django.test import TestCase
class SearchFormTestCase(TestCase):
def test_empty_get(self):
response = self.client.get(
"/en/dev/search/",
headers={"host": "docs.djangoproject.dev:8000"},
)
self.assertEqual(response.status_code, 200)
同时配置文件包含项目支持的域列表:
ALLOWED_HOSTS = ["www.djangoproject.dev", "docs.djangoproject.dev", ...]
另一个选项是使用 override_settings()
或 modify_settings()
将所需的主机添加到 ALLOWED_HOSTS
中。这个选项在不能打包自己配置文件的独立应用中可能比较好,或者对于域列表不是静态的项目(例如,多租户的子域)。例如,你可以为域 http://otherserver/
写一个测试,如下所示:
from django.test import TestCase, override_settings
class MultiDomainTestCase(TestCase):
@override_settings(ALLOWED_HOSTS=["otherserver"])
def test_other_domain(self):
response = self.client.get("http://otherserver/foo/bar/")
当运行测试时,禁用 ALLOWED_HOSTS
检查(ALLOWED_HOSTS = ['*']
),可以防止测试客户端在遵循重定向到外部 URL 时发出有用的错误信息。
测试与多数据库¶
测试主/副配置¶
如果你使用主/副本(某些数据库称为主/从)复制来测试多数据库配置,那么这种创建测试数据库的策略会带来问题。当创建测试数据库时,不会有任何复制,因此,在主服务器上创建的数据在副本上看不到。
为了弥补这一点,Django 允许你定义一个数据库是 测试镜像。考虑以下(简化的)数据库配置示例:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "myproject",
"HOST": "dbprimary",
# ... plus some other settings
},
"replica": {
"ENGINE": "django.db.backends.mysql",
"NAME": "myproject",
"HOST": "dbreplica",
"TEST": {
"MIRROR": "default",
},
# ... plus some other settings
},
}
在这个设置中,我们有两个数据库服务器。dbprimary
,用数据库别名 default
描述,dbreplica
用别名 replica
描述。正如你所期望的那样,dbreplica
被数据库管理员配置为 dbprimary
的读副本,因此在正常活动中,对 default
的任何写入都会出现在 replica
上。
如果 Django 创建了两个独立的测试数据库,就会破坏任何期望复制发生的测试。然而,replica
数据库已经被配置为测试镜像(使用 MIRROR
测试设置),表明在测试中,replica
应该被当作 default
的镜像。
在配置测试环境时,不会 创建 replica
的测试版本。相反,连接到 replica
的连接将被重定向到指向 default
。因此,对 default
的写操作将出现在 replica
上 -- 但这是因为它们实际上是同一个数据库,而不是因为两个数据库之间有数据复制。由于这依赖于事务,测试必须使用 TransactionTestCase
而不是 TestCase
。
控制测试数据库的创建顺序¶
默认情况下,Django 会假设所有的数据库都依赖于 default
数据库,因此总是先创建 default
数据库。但是,我们不保证测试配置中其他数据库的创建顺序。
如果你的数据库配置需要特定的创建顺序,你可以使用 DEPENDENCIES
测试设置指定存在的依赖关系。考虑以下(简化的)数据库配置示例:
DATABASES = {
"default": {
# ... db settings
"TEST": {
"DEPENDENCIES": ["diamonds"],
},
},
"diamonds": {
# ... db settings
"TEST": {
"DEPENDENCIES": [],
},
},
"clubs": {
# ... db settings
"TEST": {
"DEPENDENCIES": ["diamonds"],
},
},
"spades": {
# ... db settings
"TEST": {
"DEPENDENCIES": ["diamonds", "hearts"],
},
},
"hearts": {
# ... db settings
"TEST": {
"DEPENDENCIES": ["diamonds", "clubs"],
},
},
}
在这种配置下,将首先创建 diamonds
数据库,因为它是唯一没有依赖性的数据库。接下来将创建 default
和 clubs
数据库(尽管这两个数据库的创建顺序没有保证),然后是 hearts
,最后是 spades
。
如果在 DEPENDENCIES
定义中存在任何循环依赖关系,将引发 ImproperlyConfigured
异常。
TransactionTestCase
高级特性¶
- TransactionTestCase.available_apps¶
Warning
这个属性是一个私有的 API。它可能会在未来被更改或删除,而不会有废弃期,例如为了适应应用程序加载的变化。
它用来优化 Django 自己的测试套件,其中包含数百个模型,但不同应用中的模型之间没有关系。
默认情况下,
available_apps
是设置为None
。每次测试后,Django 都会调用flush
来重置数据库状态。这将清空所有表,并发出post_migrate
信号,为每个模型重新创建一个内容类型和四个权限。这个操作的花费和模型的数量成正比。将
available_apps
设置为应用程序列表会指示 Django 的行为就像只有这些应用程序的模型是可用的一样。TransactionTestCase
的行为改变如下:post_migrate
在每次测试前都会被触发,以创建可用应用中每个模型的内容类型和权限,以防它们缺失。每次测试后,Django 只清空可用应用中模型对应的表。但在数据库层面,清空表可能会级联到不可用应用中的相关模型。此外
post_migrate
并没有被触发,它将在选择了正确的应用集后,由下一个TransactionTestCase
触发。
由于数据库没有完全刷新,如果测试创建了没有包含在
available_apps
中的模型实例,它们就会泄漏,并可能导致不相关的测试失败。小心使用了会话的测试;默认的会话引擎将它们存储在数据库中。由于
post_migrate
在刷新数据库后并没有发出,所以它在一个TransactionTestCase
后的状态与一个TestCase
后的状态是不一样的:它丢失了由post_migrate
监听器创建的行。考虑到 执行测试的顺序,这并不是一个问题,只要给定的测试套件中的所有TransactionTestCase
都声明available_apps
,或者都没有声明。available_apps
在 Django 自己的测试套件中是强制性的。
- TransactionTestCase.reset_sequences¶
在
TransactionTestCase
上设置reset_sequences = True
将确保队列在测试运行前总是被重置:class TestsThatDependsOnPrimaryKeySequences(TransactionTestCase): reset_sequences = True def test_animal_pk(self): lion = Animal.objects.create(name="lion", sound="roar") # lion.pk is guaranteed to always be 1 self.assertEqual(lion.pk, 1)
除非明确测试主键序列号,否则建议你不要在测试中硬编码主键值。
使用
reset_sequences = True
会减慢测试速度,因为主键重置是一个相对昂贵的数据库操作。
强制按顺序运行测试类¶
如果你有一些测试类不能并行运行(例如,因为它们共享一个公共资源),你可以使用 django.test.testcases.SerializeMixin
来依次运行它们。这个 mixin 使用一个文件系统 lockfile
。
例如,你可以使用 __file__
来确定同一文件中所有继承自 SerializeMixin
的测试类将依次运行:
import os
from django.test import TestCase
from django.test.testcases import SerializeMixin
class ImageTestCaseMixin(SerializeMixin):
lockfile = __file__
def setUp(self):
self.filename = os.path.join(temp_storage_dir, "my_file.png")
self.file = create_file(self.filename)
class RemoveImageTests(ImageTestCaseMixin, TestCase):
def test_remove_image(self):
os.remove(self.filename)
self.assertFalse(os.path.exists(self.filename))
class ResizeImageTests(ImageTestCaseMixin, TestCase):
def test_resize_image(self):
resize_image(self.file, (48, 48))
self.assertEqual(get_image_size(self.file), (48, 48))
使用 Django 测试运行器测试可重用的应用程序¶
如果你正在编写一个 可重用的应用程序,你可能想使用 Django 测试运行器来运行你自己的测试套件,从而从 Django 测试基础设施中获益。
常见的做法是在应用程序代码旁边创建一个 tests 目录,具有以下结构:
runtests.py
polls/
__init__.py
models.py
...
tests/
__init__.py
models.py
test_settings.py
tests.py
让我们看一下其中的两个文件:
#!/usr/bin/env python
import os
import sys
import django
from django.conf import settings
from django.test.utils import get_runner
if __name__ == "__main__":
os.environ["DJANGO_SETTINGS_MODULE"] = "tests.test_settings"
django.setup()
TestRunner = get_runner(settings)
test_runner = TestRunner()
failures = test_runner.run_tests(["tests"])
sys.exit(bool(failures))
这是运行测试套件的脚本。它设置 Django 环境,创建测试数据库并运行测试。
为了清楚起见,这个例子只包含了使用 Django 测试运行器所需的最基本的内容。你可能会要添加命令行选项来控制详细程度,传递要运行的特定测试标签等。
SECRET_KEY = "fake-key"
INSTALLED_APPS = [
"tests",
]
该文件包含运行应用程序测试所需的 Django 配置。
再次,这是一个最小的例子;你的测试可能需要其他设置才能运行。
由于 tests 包在运行测试时被包含在 INSTALLED_APPS
中,你可以在它的 models.py
文件中定义只用于测试的模型。
使用不同的测试框架¶
显然,unittest
并不是唯一的 Python 测试框架。虽然 Django 并没有提供对替代框架的明确支持,但它确实提供了一种方法来调用为替代框架构建的测试,就像它们是正常的 Django 测试一样。
当你运行 ./manage.py test
时,Django 会查看 TEST_RUNNER
的配置来决定做什么。默认情况下, TEST_RUNNER
指向 'django.test.runner.DiscoverRunner'
。这个类定义了默认的 Django 测试行为。这个行为包括:
进行全局性的测试前设置。
在当前目录下的任何文件中寻找名称符合
test*.py
模式的测试。创建测试数据库。
运行
migrate
将模型和初始数据安装到测试数据库中。运行 系统检查。
运行找到的测试。
销毁测试数据库。
进行全局性的测试后拆解。
如果你定义了自己的测试运行器类,并将 TEST_RUNNER
指向该类,那么每当你运行 ./manage.py test
时,Django 就会执行你的测试运行器。通过这种方式,可以使用任何可以从 Python 代码中执行的测试框架,也可以修改 Django 测试执行过程来满足你的任何测试需求。
定义测试运行器¶
测试运行器是一个类,他定义了 run_tests()
方法。Django 自带一个 DiscoverRunner
类,它定义了默认的 Django 测试行为。该类定义了进入点 run_tests()
,再加上对 run_tests()
所使用的其他方法的选择,以此来建立,执行和拆除测试套件。
- class DiscoverRunner(pattern='test*.py', top_level=None, verbosity=1, interactive=True, failfast=False, keepdb=False, reverse=False, debug_mode=False, debug_sql=False, parallel=0, tags=None, exclude_tags=None, test_name_patterns=None, pdb=False, buffer=False, enable_faulthandler=True, timing=True, shuffle=False, logger=None, durations=None, **kwargs)[source]¶
DiscoverRunner
将在任何符合pattern
的文件中搜索测试。top_level
可以用来指定包含顶级 Python 模块的目录。通常 Django 会自动计算出这个目录,所以不需要指定这个选项。如果指定了这个选项,一般来说,它应该是包含你的manage.py
文件的目录。verbosity
决定将打印到控制台的通知和调试信息的数量;0
为无输出,1
为正常输出,2
为详细输出。如果
interactive
是True
,则测试套件在执行测试套件时,有权限向用户请求指令。这种行为的一个例子是要求允许删除一个现有的测试数据库。如果interactive
为False
,测试套件必须能够在没有任何人工干预的情况下运行。如果
failfast
为True
,测试套件将在检测到第一次测试失败后停止运行。如果
keepdb
为True
,测试套件将使用现有数据库,或在必要时创建一个数据库。如果False
,将创建一个新的数据库,并提示用户删除现有的数据库。如果
reverse
为True
,则测试用例将按相反的顺序执行。这对于调试未正确隔离并具有副作用的测试非常有用。在使用此选项时,仍然会保留按测试类分组的方式(按测试类分组)。此选项可以与--shuffle
一起使用,以反转特定随机种子的顺序。debug_mode
指定DEBUG
设置在运行测试之前应该设置成什么。parallel
指定了进程的数量。如果parallel
大于1
,测试套件将在parallel
个进程中运行。如果测试用例类的数量少于配置的进程数,Django 将相应地减少进程数。每个进程都有自己的数据库。此选项需要第三方包tblib
以正确显示回溯信息。tags
用于指定一系列 测试标签。可以与exclude_tags
结合使用。exclude_tags
用于指定一系列 排除测试标签。可以与tags
结合使用。如果
debug_sql
为True
,失败的测试用例会输出 SQL 查询记录到 django.db.backends logger 以及回溯。如果verbosity
是2
,那么所有测试中的查询都会输出。test_name_patterns
可以用来指定一套模式,通过名称过滤测试方法和类。如果
pdb
为True
,则每次测试错误或失败时都会产生一个调试器(pdb
或ipdb
)。如果
buffer
为True
,通过测试的输出将被丢弃。如果
enable_faulthandler
是True
,那么faulthandler
将被启用。如果
timing
是True
,将显示测试时间,包括数据库设置和总运行时间。如果
shuffle
是一个整数,测试用例将在执行之前以随机顺序进行洗牌,使用整数作为随机种子。如果shuffle
是None
,则将随机生成种子。在这两种情况下,种子将在运行测试之前被记录并设置为self.shuffle_seed
。此选项可以用于帮助检测未正确隔离的测试。在使用此选项时,将保留按测试类分组的方式(按测试类分组)。logger
可以用于传递一个 Python Logger 对象。如果提供了该对象,将使用它来记录消息,而不是打印到控制台。日志记录对象将遵循其日志记录级别,而不是verbosity
。durations
会显示 N 个最慢的测试用例列表。将此选项设置为0
将显示所有测试的持续时间。需要 Python 3.12+。Django 可能会不时地通过添加新的参数来扩展测试运行器的功能。
**kwargs
声明允许这种扩展。如果你将DiscoverRunner
子类化,或者编写你自己的测试运行器,确保它接受**kwargs
。你的测试运行器也可以定义额外的命令行选项。创建或覆盖一个
add_arguments(cls, parser)
类方法,并通过在该方法中调用parser.add_argument()
来添加自定义参数,这样test
命令就可以使用这些参数。New in Django 5.0:已添加
durations
参数。
属性¶
- DiscoverRunner.test_suite¶
用于构建测试套件的类。默认情况下,它被设置为
unittest.TestSuite
。如果你想实现不同的测试收集逻辑,可以重写这个类。
- DiscoverRunner.test_runner¶
这是低级测试运行器的类,用于执行各个测试和格式化结果。默认情况下,它被设置为
unittest.TextTestRunner
。尽管在命名习惯上有不幸的相似之处,但这与DiscoverRunner
不是同一类型的类,后者涵盖了更广泛的职责。你可以覆盖这个属性来修改测试运行和报告的方式。
- DiscoverRunner.test_loader¶
这是一个加载测试的类,无论是从 TestCases 还是模块或其他方面加载测试,并将它们捆绑成测试套件供运行者执行。默认情况下,它被设置为
unittest.defaultTestLoader
。如果你的测试要以不寻常的方式加载,你可以重写这个属性。
方法¶
- DiscoverRunner.run_tests(test_labels, **kwargs)[source]¶
运行测试套件。
test_labels
允许你指定要运行的测试,并支持多种格式(参见DiscoverRunner.build_suite()
获取支持的格式列表)。这个方法应该返回失败的测试次数。
- classmethod DiscoverRunner.add_arguments(parser)[source]¶
重写这个类方法来添加
test
管理命令接受的自定义参数。参见argparse.ArgumentParser.add_argument()
了解关于向解析器添加参数的详细信息。
- DiscoverRunner.setup_test_environment(**kwargs)[source]¶
通过调用
setup_test_environment()
和设置DEBUG
为self.debug_mode
(默认为False
)来设置测试环境。
- DiscoverRunner.build_suite(test_labels=None, **kwargs)[source]¶
构建一个与提供的测试标签相匹配的测试套件。
test_labels
是描述要运行的测试的字符串列表。测试标签可以采取以下四种形式之一:path.to.test_module.TestCase.test_method
-- 运行测试用例类中的单个测试方法。path.to.test_module.TestCase
——运行测试用例中的所有测试方法。path.to.module
——搜索并运行命名的 Python 包或模块中的所有测试。path/to/directory
——搜索并运行指定目录下的所有测试。
如果
test_labels
的值为None
,测试运行器将在当前目录下所有文件中搜索名称符合pattern
的测试(见上文)。返回一个准备运行的
TestSuite
实例。
- DiscoverRunner.setup_databases(**kwargs)[source]¶
通过调用
setup_databases()
创建测试数据库。
- DiscoverRunner.teardown_databases(old_config, **kwargs)[source]¶
通过调用
trapdown_databases()
来销毁测试数据库,恢复测试前的条件。
- DiscoverRunner.log(msg, level=None)[source]¶
如果设置了
logger
,则以给定的整数 logging level (例如logging.DEBUG
、logging.INFO
或logging.WARNING
)记录消息。否则,消息将打印到控制台,并考虑当前的verbosity
。例如,如果verbosity
为 0,则不会打印任何消息;如果verbosity
至少为 1,则将打印INFO
及以上的消息;如果至少为 2,则会打印DEBUG
。level
默认为logging.INFO
。
测试工具集¶
django.test.utils
¶
为了帮助创建自己的测试运行器,Django 在 django.test.utils
模块中提供了一些实用的方法。
- setup_test_environment(debug=None)[source]¶
执行全局性的测试前设置,如为模板渲染系统安装仪器,设置虚拟的电子邮件发件箱。
如果
debug
不是None
,则DEBUG
配置更新为其值。
- setup_databases(verbosity, interactive, *, time_keeper=None, keepdb=False, debug_sql=False, parallel=0, aliases=None, serialized_aliases=None, **kwargs)[source]¶
创建测试数据库。
返回一个数据结构,该结构提供了足够的细节来撤销已做的更改。这些数据将在测试结束后提供给
teardown_databases()
函数。aliases
参数确定应为哪些DATABASES
别名设置测试数据库。如果未提供,默认为所有DATABASES
别名。serialized_aliases
参数确定哪些子集的aliases
应将其状态序列化以允许使用 serialized_rollback 功能。如果未提供,默认为aliases
。serialized_aliases
参数确定哪些子集的aliases
应将其状态序列化以允许使用 serialized_rollback 功能。如果未提供,默认为aliases
。
- teardown_databases(old_config, parallel=0, keepdb=False)[source]¶
销毁测试数据库,恢复测试前的条件。
old_config
是一个数据结构,定义了数据库配置中需要撤销的变化。它是setup_databases()
方法的返回值。
django.db.connection.creation
¶
数据库后台的创建模块还提供了一些在测试过程中有用的实用程序。
- create_test_db(verbosity=1, autoclobber=False, serialize=True, keepdb=False)¶
创建一个新的测试数据库并对其运行
migrate
。verbosity
与run_tests()
中的行为相同。autoclobber
描述了在发现与测试数据库同名的数据库时将发生的行为。如果
autoclobber
为False
,将要求用户批准销毁现有数据库。如果用户不同意,则调用sys.exit
。如果
autoclobber
为True
,数据库将被销毁,而无需与用户协商。
serialize
决定 Django 是否在运行测试之前将数据库序列化为内存中的 JSON 字符串(如果没有事务,用于在测试之间恢复数据库状态)。如果你没有使用 serialized_rollback=True 的测试类,你可以将其设置为False
以加快创建时间。keepdb
决定测试运行是否应使用现有数据库,还是创建一个新的数据库。如果 True`,则使用现有的数据库,如果不存在,则创建新的数据库。如果False
,则创建一个新的数据库,并提示用户删除现有的数据库(如果存在)。返回其创建的测试数据库的名称。
create_test_db()
的副作用是修改DATABASES
中的NAME
的值,使其与测试数据库的名称相匹配。
- destroy_test_db(old_database_name, verbosity=1, keepdb=False)¶
销毁名称为
DATABASES
中NAME
值的数据库,并将NAME
设置为old_database_name
值。verbosity
参数和测试类DiscoverRunner
的行为一样。如果
keepdb
的参数为True
,数据库连接会被关闭,但是数据库不会被销毁。
集成 coverage.py
¶
代码覆盖度表示有多少源代码被测试了。它表明了代码的哪些部分被测试用例覆盖,哪些没有。这是测试应用很重要的部分,所以强烈推荐检查测试用例的覆盖度。
Django 可以很容易地与 coverage.py 集成,这是一个用于测量 Python 程序代码覆盖率的工具。首先,安装 coverage。然后,从包含 manage.py
的项目文件夹运行以下命令:
coverage run --source='.' manage.py test myapp
这将运行你的测试并收集项目中执行的文件的覆盖数据。你可以通过输入以下命令来查看这些数据的报告:
coverage report
请注意一些Django代码会在运行期间被执行,但是因为在上一条命令中没有 source
选项所以在这里没有列出。
关于类似于输出详细内容的HTML列举的没有覆盖区域的选项,请查阅 coverage.py 的文档。