单元测试集¶
Django在代码基本库的 tests
目录中带有自己的测试套件。 我们的政策是确保所有测试始终通过。
我们感谢对测试套件的所有贡献!
Django 的测试都使用随 Django 一起提供的测试基础结构来测试应用程序。请查看 编写并运行测试 以了解如何编写新的测试。
运行单元测试¶
快速上手¶
首先,在 GitHub 上 fork Django。
第二步,创建并激活虚拟环境。如果你不熟悉如何做,可以阅读我们的 贡献教程。
接下来,克隆您的 fork,安装一些要求,然后运行测试:
$ git clone https://github.com/YourGitHubName/django.git django-repo
$ cd django-repo/tests
$ python -m pip install -e ..
$ python -m pip install -r requirements/py3.txt
$ ./runtests.py
...\> git clone https://github.com/YourGitHubName/django.git django-repo
...\> cd django-repo\tests
...\> py -m pip install -e ..
...\> py -m pip install -r requirements\py3.txt
...\> runtests.py
安装这些要求可能需要一些操作系统包,您的计算机可能没有安装。通常,您可以通过搜索错误消息的最后一行或附近的信息来找出需要安装的包。如果需要的话,可以在搜索查询中添加您的操作系统信息。
如果安装这些要求遇到困难,您可以跳过这一步。请参阅 运行所有测试 以了解如何安装可选的测试依赖项的详细信息。如果您没有安装某个可选的依赖项,将会跳过需要它的测试。
运行测试需要一个定义要使用的数据库的 Django 设置模块。为了帮助您入门,Django 提供并使用一个示例设置模块,使用 SQLite 数据库。请参阅 使用另一个 settings 配置模块 了解如何使用不同的设置模块以不同的数据库运行测试。
遇到问题了吗?请查看 错误调试 以了解一些常见问题。
使用 tox
运行测试¶
Tox 是一个在不同虚拟环境中运行测试的工具。Django 包含一个基本的 tox.ini
文件,用于自动执行一些我们的构建服务器在拉取请求上执行的检查。要运行单元测试和其他检查(如 import sorting、documentation spelling checker 和 code formatting),请在 Django 源代码树的任何位置安装并运行 tox
命令:
$ python -m pip install tox
$ tox
...\> py -m pip install tox
...\> tox
默认情况下,tox
使用 SQLite 的捆绑测试设置文件运行测试套件,还会运行 black
、blacken-docs
、flake8
、isort
和文档拼写检查工具。除了此文档的其他地方提到的系统依赖项之外,python3
命令必须位于您的路径上并链接到适当版本的 Python。默认环境列表如下:
$ tox -l
py3
black
blacken-docs
flake8>=3.7.0
docs
isort>=5.1.0
...\> tox -l
py3
black
blacken-docs
flake8>=3.7.0
docs
isort>=5.1.0
测试其他 Python 版本和数据库后端¶
除了默认的环境外,tox
还支持运行其他版本的 Python 和其他数据库后端的单元测试。但由于 Django 的测试套件不提供除 SQLite 外的数据库后端的设置文件,因此您必须 创建并提供自己的测试设置。例如,要在 Python 3.10 上使用 PostgreSQL 运行测试:
$ tox -e py310-postgres -- --settings=my_postgres_settings
...\> tox -e py310-postgres -- --settings=my_postgres_settings
此命令设置一个 Python 3.10 的虚拟环境,安装 Django 的测试套件依赖项(包括 PostgreSQL 的依赖项),然后使用提供的参数(在本例中为 --settings=my_postgres_settings
)调用 runtests.py
。
本文档的其余部分展示了如何在没有 tox
的情况下运行测试的命令,但是,可以通过在参数列表前面加上 --
来将任何选项传递给 tox
,就像上面一样。
Tox
还会尊重设置的 DJANGO_SETTINGS_MODULE
环境变量。例如,以下命令与上面的命令等效:
$ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py310-postgres
对于 Windows 用户应使用:
...\> set DJANGO_SETTINGS_MODULE=my_postgres_settings
...\> tox -e py310-postgres
运行 JavaScript 测试集¶
Django 包含了一组针对某些附加应用程序中的功能的 JavaScript 单元测试。这些 JavaScript 测试默认情况下不会使用 tox
运行,因为它们需要安装 Node.js
,并且对于大多数补丁来说并不是必需的。要使用 tox
运行 JavaScript 测试:
$ tox -e javascript
...\> tox -e javascript
这个命令运行 npm install
以确保测试要求是最新的,然后运行 npm test
。
使用 django-docker-box
运行测试¶
django-docker-box 允许您在所有支持的数据库和 Python 版本上运行 Django 的测试套件。有关安装和使用说明,请参阅 django-docker-box 项目页面。
使用另一个 settings
配置模块¶
包含的设置模块(tests/test_sqlite.py
)允许您使用 SQLite 运行测试套件。如果要使用不同的数据库运行测试,您需要定义自己的设置文件。一些测试,如 contrib.postgres
的测试,是特定于特定数据库后端的,如果使用不同的后端运行,则会被跳过。一些测试在特定的数据库后端上被跳过或预期失败(请查看每个后端上的 DatabaseFeatures.django_test_skips
和 DatabaseFeatures.django_test_expected_failures
)。
要使用不同的设置运行测试,请确保该模块位于您的 PYTHONPATH
上,并使用 --settings
传递该模块。
在任何测试设置模块中,DATABASES
设置需要定义两个数据库:
一个
default
数据库。该数据库应该使用您希望用于主要测试的后端。一个带有别名
other
的数据库。other
数据库用于测试可以将查询定向到不同的数据库。该数据库应该使用与default
相同的后端,但必须具有不同的名称。
如果您使用的不是SQLite后端,则需要为每个数据库提供其他详细信息:
测试数据库的名称是通过将 DATABASES
中定义的数据库的 NAME
设置的值前面添加 test_
来获取的。这些测试数据库在测试完成时被删除。
您还需要确保您的数据库使用 UTF-8 作为默认字符集。如果您的数据库服务器不使用 UTF-8 作为默认字符集,您需要在适用数据库的测试设置字典中为 CHARSET
包含一个值。
执行一部分测试¶
Django的整个测试套件需要花一些时间才能运行,并且,例如,如果您刚刚向Django添加了一个想要快速运行而不运行其他所有功能的测试,则运行每个测试可能是多余的。您可以通过在命令行上将测试模块的名称附加到 runtests.py
上来运行单元测试的子集。
例如,如果您想仅运行有关通用关系和国际化的测试,请键入:
$ ./runtests.py --settings=path.to.settings generic_relations i18n
...\> runtests.py --settings=path.to.settings generic_relations i18n
如何找出各个测试的名称?查看 tests/
- 那里的每个目录名称都是一个测试的名称。
如果您只想运行特定类别的测试,可以指定一个包含单独测试类路径的列表。例如,要运行 i18n
模块的 TranslationTests
,请输入:
$ ./runtests.py --settings=path.to.settings i18n.tests.TranslationTests
...\> runtests.py --settings=path.to.settings i18n.tests.TranslationTests
更进一步,您可以像这样指定一个单独的测试方法:
$ ./runtests.py --settings=path.to.settings i18n.tests.TranslationTests.test_lazy_objects
...\> runtests.py --settings=path.to.settings i18n.tests.TranslationTests.test_lazy_objects
您可以使用 --start-at
选项从指定的顶级模块开始运行测试。例如:
$ ./runtests.py --start-at=wsgi
...\> runtests.py --start-at=wsgi
您还可以使用 --start-after
选项从指定的顶级模块之后开始运行测试。例如:
$ ./runtests.py --start-after=wsgi
...\> runtests.py --start-after=wsgi
请注意,--reverse
选项不影响 --start-at
或 --start-after
选项。而且,这些选项不能与测试标签一起使用。
运行 Selenium 测试¶
一些测试需要 Selenium 和一个 Web 浏览器。要运行这些测试,您必须安装 selenium 包,并使用 --selenium=<BROWSERS>
选项运行测试。例如,如果您安装了 Firefox 和 Google Chrome:
$ ./runtests.py --selenium=firefox,chrome
...\> runtests.py --selenium=firefox,chrome
查看 selenium.webdriver 包,了解可用浏览器的列表。
指定 --selenium
会自动设置 --tags=selenium
,以仅运行需要 selenium 的测试。
一些浏览器(例如 Chrome 或 Firefox)支持无头测试,这可以更快速和更稳定。添加 --headless
选项以启用此模式。
For testing changes to the admin UI, the selenium tests can be run with the
--screenshots
option enabled. Screenshots will be saved to the
tests/screenshots/
directory.
To define when screenshots should be taken during a selenium test, the test
class must use the @django.test.selenium.screenshot_cases
decorator with a
list of supported screenshot types ("desktop_size"
, "mobile_size"
,
"small_screen_size"
, "rtl"
, "dark"
, and "high_contrast"
). It
can then call self.take_screenshot("unique-screenshot-name")
at the desired
point to generate the screenshots. For example:
from django.test.selenium import SeleniumTestCase, screenshot_cases
from django.urls import reverse
class SeleniumTests(SeleniumTestCase):
@screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
def test_login_button_centered(self):
self.selenium.get(self.live_server_url + reverse("admin:login"))
self.take_screenshot("login")
...
This generates multiple screenshots of the login page - one for a desktop screen, one for a mobile screen, one for right-to-left languages on desktop, one for the dark mode on desktop, and one for high contrast mode on desktop when using chrome.
The --screenshots
option and @screenshot_cases
decorator were
added.
运行所有测试¶
如果要运行全套测试,则需要安装许多依赖项:
argon2-cffi 19.2.0+
asgiref 3.8.1+ (required)
colorama 0.4.6+
docutils 0.19+
Jinja2 2.11+
Pillow 6.2.1+
redis 3.4+
selenium 4.8.0+
sqlparse 0.3.1+ (必须的)
tblib 1.5.0+
你可以在 Django 源代码树的 tests/requirements
目录中找到这些依赖项的 pip requirements 文件,然后像这样安装它们:
$ python -m pip install -r tests/requirements/py3.txt
...\> py -m pip install -r tests\requirements\py3.txt
如果你在安装过程中遇到错误,你的系统可能缺少一个或多个 Python 包的依赖。请查阅失败软件包的文档,或者用你遇到的错误信息在网上搜索。
你还可以使用 oracle.txt
、mysql.txt
或 postgres.txt
安装你选择的数据库适配器。
如果要测试 memcached 或 Redis 缓存后端,你还需要定义一个 CACHES
设置,分别指向你的 memcached 或 Redis 实例。
要运行 GeoDjango 测试,你需要 设置一个空间数据库并安装地理空间库。
这些依赖项中的每一个都是可选的。 如果您缺少其中的任何一个,则将跳过关联的测试。
要运行一些自动重新加载测试,你需要安装 Watchman 服务。
代码覆盖率¶
鼓励贡献者对测试套件进行覆盖率测试,以确定需要额外测试的区域。在:ref:testing code coverage 1. 中介绍了coverage代码覆盖度工具的安装和使用。
要使用标准测试设置运行 Django 测试套件的覆盖率:
$ coverage run ./runtests.py --settings=test_sqlite
...\> coverage run runtests.py --settings=test_sqlite
在运行覆盖率后,通过运行以下命令合并所有覆盖率统计数据:
$ coverage combine
...\> coverage combine
然后通过运行以下命令生成 HTML 报告:
$ coverage html
...\> coverage html
运行Django测试的覆盖率时,随附的 .coveragerc
配置文件将 coverage_html
定义为报告的输出目录,并且还排除了与结果无关的几个目录(Django中包含的测试代码或外部代码)。
Contrib 应用程序¶
contrib 应用程序的测试可以在 tests/ 目录中找到,通常位于 <app_name>_tests
下。例如,contrib.auth
的测试位于 tests/auth_tests 中。
错误调试¶
测试套件在 main
分支上出现挂起或失败。¶
确保你使用的是一个 支持的 Python 版本 的最新点发布版,因为早期版本中通常会有可能导致测试套件失败或挂起的错误。
在 macOS (High Sierra 和更新的版本)上,你可能会看到这个消息被记录下来,之后测试会挂起:
objc[42074]: +[__NSPlaceholderDate initialize] may have been in progress in
another thread when fork() was called.
为了避免这个问题,设置一个 OBJC_DISABLE_INITIALIZE_FORK_SAFETY
环境变量,例如:
$ OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES ./runtests.py
或者将 export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
添加到你的 shell 启动文件(例如 ~/.profile
)。
许多测试失败,出现了 UnicodeEncodeError
。¶
如果未安装 locales
包,一些测试将会因为 UnicodeEncodeError
而失败。
你可以在基于 Debian 的系统上解决这个问题,例如,通过运行以下命令:
$ apt-get install locales
$ dpkg-reconfigure locales
你可以在 macOS 系统上通过配置你的 shell 的 locale 来解决这个问题:
$ export LANG="en_US.UTF-8"
$ export LC_ALL="en_US.UTF-8"
运行 locale
命令来确认更改。如果需要,可以将这些导出命令添加到你的 shell 启动文件(例如,对于 Bash,是 ~/.bashrc
)中,以避免需要重新输入它们。
仅在特定组合条件下失败的测试。¶
如果一个测试在单独运行时通过,但在整个测试套件中失败,我们有一些工具可以帮助分析问题。
使用 runtests.py
的 --bisect
选项将在每次迭代中运行失败的测试,同时减半运行的测试集合,通常可以帮助识别与失败相关的少量测试。
例如,假设单独运行正常的失败测试是 ModelTest.test_eq
,然后使用以下命令:
$ ./runtests.py --bisect basic.tests.ModelTest.test_eq
...\> runtests.py --bisect basic.tests.ModelTest.test_eq
将尝试确定与给定测试干扰的测试。首先,将使用测试套件的前半部分运行该测试。如果出现失败,将测试套件的前半部分分成两组,然后每组都将与指定的测试一起运行。如果在测试套件的前半部分没有失败,将使用指定的测试运行测试套件的后半部分,并根据前述方式进行适当的分割。该过程将重复,直到最小化失败测试的集合。
--pair
选项会将给定的测试与测试套件中的每个其他测试一起运行,让你检查是否有其他测试产生了导致失败的副作用。因此:
$ ./runtests.py --pair basic.tests.ModelTest.test_eq
...\> runtests.py --pair basic.tests.ModelTest.test_eq
将使用 test_eq
与每个测试标签配对。
使用 --bisect
和 --pair
,如果你已经怀疑哪些情况可能导致失败,你可以在第一个标签之后通过 指定更多的测试标签 来限制进行交叉分析的测试。
$ ./runtests.py --pair basic.tests.ModelTest.test_eq queries transactions
...\> runtests.py --pair basic.tests.ModelTest.test_eq queries transactions
你还可以尝试使用 --shuffle
和 --reverse
选项以随机或反向顺序运行任何一组测试。这有助于验证以不同的顺序执行测试是否会导致任何问题:
$ ./runtests.py basic --shuffle
$ ./runtests.py basic --reverse
...\> runtests.py basic --shuffle
...\> runtests.py basic --reverse
查看测试期间运行的 SQL 查询。¶
如果你希望查看失败测试中运行的 SQL,你可以使用 --debug-sql
选项打开 SQL 日志记录。如果将其与 --verbosity=2
结合使用,将输出所有的 SQL 查询:
$ ./runtests.py basic --debug-sql
...\> runtests.py basic --debug-sql
查看测试失败的完整回溯信息。¶
默认情况下,测试会并行运行,每个核心一个进程。然而,在并行运行测试时,对于任何测试失败,你只会看到一个截断的回溯信息。你可以使用 --parallel
选项来调整这个行为:
$ ./runtests.py basic --parallel=1
...\> runtests.py basic --parallel=1
你还可以使用 DJANGO_TEST_PROCESSES
环境变量来实现这个目的。
编写测试的一些建议¶
隔离模型注册¶
为了避免污染全局的 :attr:`~django.apps.apps 注册表并防止不必要的表创建,测试方法中定义的模型应该与临时的 Apps
实例绑定。要做到这一点,可以使用 isolate_apps()
装饰器:
from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps
class TestModelDefinition(SimpleTestCase):
@isolate_apps("app_label")
def test_model_definition(self):
class TestModel(models.Model):
pass
...
设置 app_label
在测试方法中定义的模型,如果没有显式设置 app_label
,将自动分配给其所在测试类所在的应用程序的标签。
为了确保在 isolate_apps()
实例的上下文中定义的模型被正确安装,你应该将目标的 app_label
集合作为参数传递:
from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps
class TestModelDefinition(SimpleTestCase):
@isolate_apps("app_label", "other_app_label")
def test_model_definition(self):
# This model automatically receives app_label='app_label'
class TestModel(models.Model):
pass
class OtherAppModel(models.Model):
class Meta:
app_label = "other_app_label"
...