进阶测试主题
请求工厂
class RequestFactory
RequestFactory 与测试客户端共享相同的 API。 但是,RequestFactory 不能像浏览器那样运行,而是提供一种生成请求实例的方法,该实例可用作任何视图的第一个参数。 这意味着您可以像测试任何其他功能一样测试视图函数——就像一个黑匣子一样,具有确切已知的输入,可以测试特定的输出。
RequestFactory 的 API 是测试客户端 API 的一个稍加限制的子集。
- 它只能访问 HTTP 的 get()、post()、put()、delete()、head()、options() 和 trace() 方法。
- 这些方法接受所有相同的参数,除了
follow
。因为这只是一个产生请求的工厂,所以由你来处理响应。 - 它不支持中间件。如果需要视图正常运行,会话和认证属性必须由测试本身提供。
Changed in Django 4.2:
The headers
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
class AsyncRequestFactory
RequestFactory
创建 WSGI 类的请求。如果你想创建 ASGI 类的请求,包括有一个正确的 ASGI scope
,你可以使用 django.test.AsyncRequestFactory
。
该类与 RequestFactory
直接 API 兼容,唯一的区别是它返回 ASGIRequest
实例,而不是 WSGIRequest
实例。它的所有方法仍然是可同步调用的。
Arbitrary keyword arguments in defaults
are added directly into the ASGI scope.
Changed in Django 4.2:
The headers
parameter was added.
测试基于类的视图
为了在请求/响应周期之外测试基于类的视图,你必须确保它们配置正确,在实例化之后调用 setup()
。
例如,假设基于类的视图如下:
views.py
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
,然后再进行测试代码。
tests.py
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
的镜像。
When the test environment is configured, a test version of replica
will not be created. Instead the connection to replica
will be redirected to point at default
. As a result, writes to default
will appear on replica
— but because they are actually the same database, not because there is data replication between the two databases. As this depends on transactions, the tests must use TransactionTestCase instead of 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
警告
这个属性是一个私有的 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 测试基础设施中获益。
A common practice is a tests directory next to the application code, with the following structure:
runtests.py
polls/
__init__.py
models.py
...
tests/
__init__.py
models.py
test_settings.py
tests.py
让我们看一下其中的两个文件:
runtests.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 测试运行器所需的最基本的内容。你可能会要添加命令行选项来控制详细程度,传递要运行的特定测试标签等。
tests/test_settings.py
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)
DiscoverRunner
将在任何符合 pattern
的文件中搜索测试。
top_level
可以用来指定包含顶级 Python 模块的目录。通常 Django 会自动计算出这个目录,所以不需要指定这个选项。如果指定了这个选项,一般来说,它应该是包含你的 manage.py
文件的目录。
verbosity
决定将打印到控制台的通知和调试信息的数量;0
为无输出,1
为正常输出,2
为详细输出。
如果 interactive
是 True
,则测试套件在执行测试套件时,有权限向用户请求指令。这种行为的一个例子是要求允许删除一个现有的测试数据库。如果 interactive
为 False
,测试套件必须能够在没有任何人工干预的情况下运行。
如果 failfast
为 True
,测试套件将在检测到第一次测试失败后停止运行。
如果 keepdb
为 True
,测试套件将使用现有数据库,或在必要时创建一个数据库。如果 False
,将创建一个新的数据库,并提示用户删除现有的数据库。
If reverse
is True
, test cases will be executed in the opposite order. This could be useful to debug tests that aren’t properly isolated and have side effects. Grouping by test class is preserved when using this option. This option can be used in conjunction with --shuffle
to reverse the order for a particular random seed.
debug_mode
指定 DEBUG 设置在运行测试之前应该设置成什么。
parallel
specifies the number of processes. If parallel
is greater than 1
, the test suite will run in parallel
processes. If there are fewer test case classes than configured processes, Django will reduce the number of processes accordingly. Each process gets its own database. This option requires the third-party tblib
package to display tracebacks correctly.
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
,将显示测试时间,包括数据库设置和总运行时间。
If shuffle
is an integer, test cases will be shuffled in a random order prior to execution, using the integer as a random seed. If shuffle
is None
, the seed will be generated randomly. In both cases, the seed will be logged and set to self.shuffle_seed
prior to running tests. This option can be used to help detect tests that aren’t properly isolated. Grouping by test class is preserved when using this option.
logger
can be used to pass a Python Logger object. If provided, the logger will be used to log messages instead of printing to the console. The logger object will respect its logging level rather than the verbosity
.
durations
will show a list of the N slowest test cases. Setting this option to 0
will result in the duration for all tests being shown. Requires Python 3.12+.
Django 可能会不时地通过添加新的参数来扩展测试运行器的功能。**kwargs
声明允许这种扩展。如果你将 DiscoverRunner
子类化,或者编写你自己的测试运行器,确保它接受 **kwargs
。
你的测试运行器也可以定义额外的命令行选项。创建或覆盖一个 add_arguments(cls, parser)
类方法,并通过在该方法中调用 parser.add_argument()
来添加自定义参数,这样 test 命令就可以使用这些参数。
New in Django 5.0:
The durations
argument was added.
属性
DiscoverRunner.test_suite
用于构建测试套件的类。默认情况下,它被设置为 unittest.TestSuite
。如果你想实现不同的测试收集逻辑,可以重写这个类。
DiscoverRunner.test_runner
这是低级测试运行器的类,用于执行各个测试和格式化结果。默认情况下,它被设置为 unittest.TextTestRunner
。尽管在命名习惯上有不幸的相似之处,但这与 DiscoverRunner
不是同一类型的类,后者涵盖了更广泛的职责。你可以覆盖这个属性来修改测试运行和报告的方式。
DiscoverRunner.test_loader
这是一个加载测试的类,无论是从 TestCases 还是模块或其他方面加载测试,并将它们捆绑成测试套件供运行者执行。默认情况下,它被设置为 unittest.defaultTestLoader
。如果你的测试要以不寻常的方式加载,你可以重写这个属性。
方法
DiscoverRunner.run_tests
(test_labels, **kwargs)
运行测试套件。
test_labels
允许你指定要运行的测试,并支持多种格式(参见 DiscoverRunner.build_suite() 获取支持的格式列表)。
这个方法应该返回失败的测试次数。
classmethod DiscoverRunner.add_arguments
(parser)
重写这个类方法来添加 test 管理命令接受的自定义参数。参见 argparse.ArgumentParser.add_argument() 了解关于向解析器添加参数的详细信息。
DiscoverRunner.setup_test_environment
(**kwargs)
通过调用 setup_test_environment() 和设置 DEBUG 为 self.debug_mode
(默认为 False
)来设置测试环境。
DiscoverRunner.build_suite
(test_labels=None, **kwargs)
构建一个与提供的测试标签相匹配的测试套件。
test_labels
是描述要运行的测试的字符串列表。测试标签可以采取以下四种形式之一:
path.to.test_module.TestCase.test_method
— Run a single test method in a test case class.path.to.test_module.TestCase
——运行测试用例中的所有测试方法。path.to.module
——搜索并运行命名的 Python 包或模块中的所有测试。path/to/directory
——搜索并运行指定目录下的所有测试。
如果 test_labels
的值为 None
,测试运行器将在当前目录下所有文件中搜索名称符合 pattern
的测试(见上文)。
返回一个准备运行的 TestSuite
实例。
DiscoverRunner.setup_databases
(**kwargs)
通过调用 setup_databases() 创建测试数据库。
DiscoverRunner.run_checks
(databases)
在测试的 databases
上运行 系统检查。
DiscoverRunner.run_suite
(suite, **kwargs)
运行测试套件。
返回运行测试套件所产生的结果。
DiscoverRunner.get_test_runner_kwargs
()
返回实例化 DiscoverRunner.test_runner
的关键字参数。
DiscoverRunner.teardown_databases
(old_config, **kwargs)
通过调用 trapdown_databases()
来销毁测试数据库,恢复测试前的条件。
DiscoverRunner.teardown_test_environment
(**kwargs)
恢复测试前的环境。
DiscoverRunner.suite_result
(suite, result, **kwargs)
计算并返回一个返回码,基于测试套件和测试套件返回的结果。
DiscoverRunner.log
(msg, level=None)
If a logger
is set, logs the message at the given integer logging level (e.g. logging.DEBUG
, logging.INFO
, or logging.WARNING
). Otherwise, the message is printed to the console, respecting the current verbosity
. For example, no message will be printed if the verbosity
is 0, INFO
and above will be printed if the verbosity
is at least 1, and DEBUG
will be printed if it is at least 2. The level
defaults to logging.INFO
.
测试工具集
django.test.utils
为了帮助创建自己的测试运行器,Django 在 django.test.utils
模块中提供了一些实用的方法。
setup_test_environment
(debug=None)
执行全局性的测试前设置,如为模板渲染系统安装仪器,设置虚拟的电子邮件发件箱。
如果 debug
不是 None
,则 DEBUG 配置更新为其值。
teardown_test_environment
()
进行全局性的测试后拆解,如从模板系统中删除仪器设备,恢复正常的邮件服务。
setup_databases
(verbosity, interactive, *, time_keeper=None, keepdb=False, debug_sql=False, parallel=0, aliases=None, serialized_aliases=None, **kwargs)
创建测试数据库。
返回一个数据结构,该结构提供了足够的细节来撤销已做的更改。这些数据将在测试结束后提供给 teardown_databases() 函数。
The aliases
argument determines which DATABASES aliases test databases should be set up for. If it’s not provided, it defaults to all of DATABASES aliases.
The serialized_aliases
argument determines what subset of aliases
test databases should have their state serialized to allow usage of the serialized_rollback feature. If it’s not provided, it defaults to aliases
.
teardown_databases
(old_config, parallel=0, keepdb=False)
销毁测试数据库,恢复测试前的条件。
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
。 - If
autoclobber
isTrue
, the database will be destroyed without consulting the user.
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 can be easily integrated with coverage.py, a tool for measuring code coverage of Python programs. First, install coverage. Next, run the following from your project folder containing manage.py
:
coverage run --source='.' manage.py test myapp
This runs your tests and collects coverage data of the executed files in your project. You can see a report of this data by typing following command:
coverage report
请注意一些Django代码会在运行期间被执行,但是因为在上一条命令中没有 source
选项所以在这里没有列出。
关于类似于输出详细内容的HTML列举的没有覆盖区域的选项,请查阅 coverage.py 的文档。